From 1cf2972f4598870d27f30787aec6fcaf3659fad0 Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Wed, 14 Jan 2026 18:30:10 +0100
Subject: [PATCH 01/14] feat: add backstage/ui hooks documentation
Signed-off-by: Antony Bouyon
---
docs-ui/src/app/hooks/[slug]/page.tsx | 23 +++++
docs-ui/src/app/hooks/page.mdx | 34 +++++++
.../src/components/Navigation/Navigation.tsx | 15 +++-
.../content/hooks/use-breakpoint.example.tsx | 28 ++++++
docs-ui/src/content/hooks/use-breakpoint.mdx | 44 +++++++++
.../src/content/hooks/use-breakpoint.props.ts | 37 ++++++++
.../content/hooks/use-media-query.example.tsx | 42 +++++++++
docs-ui/src/content/hooks/use-media-query.mdx | 87 ++++++++++++++++++
.../content/hooks/use-media-query.props.ts | 90 +++++++++++++++++++
docs-ui/src/utils/data.ts | 23 +++++
10 files changed, 422 insertions(+), 1 deletion(-)
create mode 100644 docs-ui/src/app/hooks/[slug]/page.tsx
create mode 100644 docs-ui/src/app/hooks/page.mdx
create mode 100644 docs-ui/src/content/hooks/use-breakpoint.example.tsx
create mode 100644 docs-ui/src/content/hooks/use-breakpoint.mdx
create mode 100644 docs-ui/src/content/hooks/use-breakpoint.props.ts
create mode 100644 docs-ui/src/content/hooks/use-media-query.example.tsx
create mode 100644 docs-ui/src/content/hooks/use-media-query.mdx
create mode 100644 docs-ui/src/content/hooks/use-media-query.props.ts
diff --git a/docs-ui/src/app/hooks/[slug]/page.tsx b/docs-ui/src/app/hooks/[slug]/page.tsx
new file mode 100644
index 0000000000..4f750a917d
--- /dev/null
+++ b/docs-ui/src/app/hooks/[slug]/page.tsx
@@ -0,0 +1,23 @@
+import { hooks } from '@/utils/data';
+
+export default async function Page({
+ params,
+}: {
+ params: Promise<{ slug: string }>;
+}) {
+ const { slug } = await params;
+
+ const { default: Component } = await import(`@/content/hooks/${slug}.mdx`);
+
+ return ;
+}
+
+export function generateStaticParams() {
+ const list = [...hooks];
+
+ return list.map(hook => ({
+ slug: hook.slug,
+ }));
+}
+
+export const dynamicParams = false;
diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx
new file mode 100644
index 0000000000..b2e2b81e7d
--- /dev/null
+++ b/docs-ui/src/app/hooks/page.mdx
@@ -0,0 +1,34 @@
+import { CodeBlock } from '@/components/CodeBlock';
+import { ComponentCard, ComponentCards } from '@/components/ComponentCards';
+
+# Hooks
+
+Backstage UI custom hooks provide easy access to design system features, style management, and responsive interface creation.
+
+
+
+
+
+
+
+
diff --git a/docs-ui/src/components/Navigation/Navigation.tsx b/docs-ui/src/components/Navigation/Navigation.tsx
index bad77e942e..f4fc51993f 100644
--- a/docs-ui/src/components/Navigation/Navigation.tsx
+++ b/docs-ui/src/components/Navigation/Navigation.tsx
@@ -11,13 +11,26 @@ import {
RiServiceLine,
RiStackLine,
} from '@remixicon/react';
-import { components } from '@/utils/data';
+import { components, hooks } from '@/utils/data';
import styles from './Navigation.module.css';
interface NavigationProps {
onLinkClick?: () => void;
}
+const data = [
+ {
+ title: 'Components',
+ content: components,
+ url: '/components',
+ },
+ {
+ title: 'Hooks',
+ content: hooks,
+ url: '/hooks',
+ },
+];
+
export const Navigation = ({ onLinkClick }: NavigationProps) => {
const pathname = usePathname();
diff --git a/docs-ui/src/content/hooks/use-breakpoint.example.tsx b/docs-ui/src/content/hooks/use-breakpoint.example.tsx
new file mode 100644
index 0000000000..d4d21861bb
--- /dev/null
+++ b/docs-ui/src/content/hooks/use-breakpoint.example.tsx
@@ -0,0 +1,28 @@
+'use client';
+
+import { useBreakpoint } from '@backstage/ui';
+import { useEffect, useState } from 'react';
+
+export function UseBreakpointExample() {
+ const { breakpoint, up, down } = useBreakpoint();
+ const [isMounted, setIsMounted] = useState(false);
+
+ // prevent hydration mismatch by rendering only on the client
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ if (!isMounted) {
+ return null;
+ }
+
+ return (
+
+
Current Breakpoint: {breakpoint}
+ {(up('md') &&
The viewport is larger than 1024px.
) ||
+ (down('sm') &&
The viewport is smaller than 768px.
) || (
+
The viewport is between 768px and 1024px.
+ )}
+
+ );
+}
diff --git a/docs-ui/src/content/hooks/use-breakpoint.mdx b/docs-ui/src/content/hooks/use-breakpoint.mdx
new file mode 100644
index 0000000000..89f5a7f757
--- /dev/null
+++ b/docs-ui/src/content/hooks/use-breakpoint.mdx
@@ -0,0 +1,44 @@
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { PageTitle } from '@/components/PageTitle';
+import { PropsTable } from '@/components/PropsTable';
+import { Snippet } from '@/components/Snippet';
+import { UseBreakpointExample } from './use-breakpoint.example';
+import {
+ useBreakpointExampleSnippet,
+ useBreakpointReturnDefs,
+} from './use-breakpoint.props';
+
+
+
+## Usage
+
+The `useBreakpoint` hook returns the active breakpoint and two functions, `up` and `down`, to check if the viewport is above, or below a given breakpoint, letting you adjust your UI responsively.
+
+}
+ code={useBreakpointExampleSnippet}
+/>
+
+## Breakpoints
+
+The default breakpoints are:
+
+- **initial**: < 640px
+- **xs**: ≥ 640px < 768px
+- **sm**: ≥ 768px < 1024px
+- **md**: ≥ 1024px < 1280px
+- **lg**: ≥ 1280px < 1536px
+- **xl**: ≥ 1536px
+
+## Return Value
+
+
+
+
diff --git a/docs-ui/src/content/hooks/use-breakpoint.props.ts b/docs-ui/src/content/hooks/use-breakpoint.props.ts
new file mode 100644
index 0000000000..298b31b3e3
--- /dev/null
+++ b/docs-ui/src/content/hooks/use-breakpoint.props.ts
@@ -0,0 +1,37 @@
+import { type PropDef } from '@/utils/propDefs';
+
+const Breakpoint = ['"initial"', '"xs"', '"sm"', '"md"', '"lg"', '"xl"'];
+
+export const useBreakpointReturnDefs: Record = {
+ breakpoint: {
+ type: 'enum',
+ values: Breakpoint,
+ description: 'The current active breakpoint based on screen width',
+ },
+ up: {
+ type: 'enum',
+ values: [`(breakpoint: ${Breakpoint.join(' | ')}) => boolean`],
+ description:
+ 'Function that takes a breakpoint and returns true if the screen width is at or above that breakpoint',
+ },
+ down: {
+ type: 'enum',
+ values: [`(breakpoint: ${Breakpoint.join(' | ')}) => boolean`],
+ description:
+ 'Function that takes a breakpoint and returns true if the screen width is at or below that breakpoint',
+ },
+};
+
+export const useBreakpointExampleSnippet = `import { useBreakpoint } from '@backstage/ui';
+
+function ResponsiveComponent() {
+ const { breakpoint, up, down } = useBreakpoint();
+
+ return (
+
+
Current Breakpoint: {breakpoint}
+ {up('md') &&
The viewport is medium or larger.
}
+ {down('sm') &&
The viewport is small or smaller.
}
+
+ );
+}`;
diff --git a/docs-ui/src/content/hooks/use-media-query.example.tsx b/docs-ui/src/content/hooks/use-media-query.example.tsx
new file mode 100644
index 0000000000..0d8edd944b
--- /dev/null
+++ b/docs-ui/src/content/hooks/use-media-query.example.tsx
@@ -0,0 +1,42 @@
+'use client';
+import { useMediaQuery } from '@backstage/ui/src/hooks/useMediaQuery';
+
+export function UseMediaQueryThemeExample() {
+ const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
+
+ return (
+
+ {isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'}
+
+ );
+}
+
+export function UseMediaQueryResponsiveExample() {
+ const isMobile = useMediaQuery('(max-width: 768px)');
+ const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
+ const isDesktop = useMediaQuery('(min-width: 1025px)');
+
+ return (
+
+ {isMobile && '
'}
+ {isTablet && '
'}
+ {isDesktop && '
'}
+
+ );
+}
+
+export function UseMediaQueryPreferencesExample() {
+ const prefersReducedMotion = useMediaQuery(
+ '(prefers-reduced-motion: reduce)',
+ );
+
+ return (
+ Content
+ );
+}
+
+export function UseMediaQueryOrientationExample() {
+ const isPortrait = useMediaQuery('(orientation: portrait)');
+
+ return {isPortrait ? 'Portrait mode' : 'Landscape mode'}
;
+}
diff --git a/docs-ui/src/content/hooks/use-media-query.mdx b/docs-ui/src/content/hooks/use-media-query.mdx
new file mode 100644
index 0000000000..6941bc0d68
--- /dev/null
+++ b/docs-ui/src/content/hooks/use-media-query.mdx
@@ -0,0 +1,87 @@
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { PageTitle } from '@/components/PageTitle';
+import { PropsTable } from '@/components/PropsTable';
+import {
+ useMediaQueryOrientationSnippet,
+ useMediaQueryParamDefs,
+ useMediaQueryPreferencesSnippet,
+ useMediaQueryResponsiveSnippet,
+ useMediaQueryReturnDefs,
+ useMediaQueryUsageSnippet,
+} from './use-media-query.props';
+import { Snippet } from '@/components/Snippet';
+import {
+ UseMediaQueryThemeExample,
+ UseMediaQueryPreferencesExample,
+ UseMediaQueryResponsiveExample,
+ UseMediaQueryOrientationExample,
+} from './use-media-query.example';
+
+
+
+## Usage
+
+The `useMediaQuery` hook allows you to evaluate CSS media queries in your components, enabling responsive design and behavior.
+
+}
+ code={useMediaQueryUsageSnippet}
+/>
+
+## Parameters
+
+
+
+## Return Value
+
+
+## Examples
+
+### Responsive Layouts
+
+Adapt layout based on different screen sizes.
+
+}
+ code={useMediaQueryResponsiveSnippet}
+/>
+
+### User Preferences
+
+Respect user system preferences.
+
+}
+ code={useMediaQueryPreferencesSnippet}
+/>
+
+### Device Orientation
+
+Detect screen orientation.
+
+}
+ code={useMediaQueryOrientationSnippet}
+/>
+
+
diff --git a/docs-ui/src/content/hooks/use-media-query.props.ts b/docs-ui/src/content/hooks/use-media-query.props.ts
new file mode 100644
index 0000000000..4a3bb1ca73
--- /dev/null
+++ b/docs-ui/src/content/hooks/use-media-query.props.ts
@@ -0,0 +1,90 @@
+import { type PropDef } from '@/utils/propDefs';
+
+export const useMediaQueryParamDefs: Record = {
+ query: {
+ type: 'string',
+ description: 'The CSS media query to evaluate',
+ },
+ options: {
+ type: 'complex',
+ complexType: {
+ name: 'UseMediaQueryOptions',
+ properties: {
+ defaultValue: {
+ type: 'boolean',
+ required: false,
+ description:
+ 'Default value to use when rendering on the server, defaults to false',
+ },
+ initializeWithValue: {
+ type: 'boolean',
+ required: false,
+ description:
+ 'Whether to initialize with the current value of the media query, or use the default value initially',
+ },
+ },
+ },
+ default: 'defaultValue: false\ninitializeWithValue: true',
+ },
+};
+
+export const useMediaQueryReturnDefs: Record = {
+ matches: {
+ type: 'boolean',
+ description: 'True if the media query currently matches',
+ },
+};
+
+export const useMediaQueryUsageSnippet = `import { useMediaQuery } from '@backstage/ui';
+
+function MyComponent() {
+ const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
+
+ return (
+
+ {isDarkMode ? 'Dark mode enabled' : 'Light mode enabled'}
+
+ );
+}`;
+
+export const useMediaQueryResponsiveSnippet = `import { useMediaQuery } from '@backstage/ui';
+
+function ResponsiveLayout() {
+ const isMobile = useMediaQuery('(max-width: 768px)');
+ const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
+ const isDesktop = useMediaQuery('(min-width: 1025px)');
+
+ return (
+
+ {isMobile &&
}
+ {isTablet &&
}
+ {isDesktop &&
}
+
+ );
+}`;
+
+export const useMediaQueryPreferencesSnippet = `import { useMediaQuery } from '@backstage/ui';
+
+function AccessibleComponent() {
+ const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
+
+ return (
+
+ Content
+
+ );
+}`;
+
+export const useMediaQueryOrientationSnippet = `import { useMediaQuery } from '@backstage/ui';
+
+function OrientationAware() {
+ const isPortrait = useMediaQuery('(orientation: portrait)');
+
+ return (
+
+ {isPortrait ? 'Portrait mode' : 'Landscape mode'}
+
+ );
+}`;
diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts
index 7426b16130..bb1ea40dfb 100644
--- a/docs-ui/src/utils/data.ts
+++ b/docs-ui/src/utils/data.ts
@@ -138,3 +138,26 @@ export const components: Page[] = [
slug: 'visually-hidden',
},
];
+
+export const hooks: Page[] = [
+ {
+ title: 'useBreakpoint',
+ slug: 'use-breakpoint',
+ },
+ {
+ title: 'useIsomorphicLayoutEffect',
+ slug: 'use-isomorphic-layout-effect',
+ },
+ {
+ title: 'useMediaQuery',
+ slug: 'use-media-query',
+ },
+ {
+ title: 'useStyles',
+ slug: 'use-styles',
+ },
+ {
+ title: 'useSurface',
+ slug: 'use-surface',
+ },
+];
From d61a7953831983ee342a094a108dffbb84fedefc Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Fri, 16 Jan 2026 17:56:30 +0100
Subject: [PATCH 02/14] refactor: migrate to the new format
Signed-off-by: Antony Bouyon
---
docs-ui/src/app/hooks/[slug]/page.tsx | 23 ------------
.../hooks/use-breakpoint/components.tsx} | 0
.../hooks/use-breakpoint/page.mdx} | 8 ++--
.../hooks/use-breakpoint/props-definition.ts} | 14 -------
.../src/app/hooks/use-breakpoint/snippets.ts | 13 +++++++
.../hooks/use-media-query/components.tsx} | 0
.../hooks/use-media-query/page.mdx} | 24 ++++++------
.../hooks/use-media-query/props-definition.ts | 36 ++++++++++++++++++
.../hooks/use-media-query/snippets.ts} | 37 -------------------
9 files changed, 67 insertions(+), 88 deletions(-)
delete mode 100644 docs-ui/src/app/hooks/[slug]/page.tsx
rename docs-ui/src/{content/hooks/use-breakpoint.example.tsx => app/hooks/use-breakpoint/components.tsx} (100%)
rename docs-ui/src/{content/hooks/use-breakpoint.mdx => app/hooks/use-breakpoint/page.mdx} (88%)
rename docs-ui/src/{content/hooks/use-breakpoint.props.ts => app/hooks/use-breakpoint/props-definition.ts} (66%)
create mode 100644 docs-ui/src/app/hooks/use-breakpoint/snippets.ts
rename docs-ui/src/{content/hooks/use-media-query.example.tsx => app/hooks/use-media-query/components.tsx} (100%)
rename docs-ui/src/{content/hooks/use-media-query.mdx => app/hooks/use-media-query/page.mdx} (95%)
create mode 100644 docs-ui/src/app/hooks/use-media-query/props-definition.ts
rename docs-ui/src/{content/hooks/use-media-query.props.ts => app/hooks/use-media-query/snippets.ts} (58%)
diff --git a/docs-ui/src/app/hooks/[slug]/page.tsx b/docs-ui/src/app/hooks/[slug]/page.tsx
deleted file mode 100644
index 4f750a917d..0000000000
--- a/docs-ui/src/app/hooks/[slug]/page.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { hooks } from '@/utils/data';
-
-export default async function Page({
- params,
-}: {
- params: Promise<{ slug: string }>;
-}) {
- const { slug } = await params;
-
- const { default: Component } = await import(`@/content/hooks/${slug}.mdx`);
-
- return ;
-}
-
-export function generateStaticParams() {
- const list = [...hooks];
-
- return list.map(hook => ({
- slug: hook.slug,
- }));
-}
-
-export const dynamicParams = false;
diff --git a/docs-ui/src/content/hooks/use-breakpoint.example.tsx b/docs-ui/src/app/hooks/use-breakpoint/components.tsx
similarity index 100%
rename from docs-ui/src/content/hooks/use-breakpoint.example.tsx
rename to docs-ui/src/app/hooks/use-breakpoint/components.tsx
diff --git a/docs-ui/src/content/hooks/use-breakpoint.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
similarity index 88%
rename from docs-ui/src/content/hooks/use-breakpoint.mdx
rename to docs-ui/src/app/hooks/use-breakpoint/page.mdx
index 89f5a7f757..47ac3ab84f 100644
--- a/docs-ui/src/content/hooks/use-breakpoint.mdx
+++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
@@ -3,11 +3,13 @@ import { CodeBlock } from '@/components/CodeBlock';
import { PageTitle } from '@/components/PageTitle';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
-import { UseBreakpointExample } from './use-breakpoint.example';
+import { UseBreakpointExample } from './components';
import {
- useBreakpointExampleSnippet,
useBreakpointReturnDefs,
-} from './use-breakpoint.props';
+} from './props-definition';
+import {
+ useBreakpointExampleSnippet
+} from './snippets';
= {
'Function that takes a breakpoint and returns true if the screen width is at or below that breakpoint',
},
};
-
-export const useBreakpointExampleSnippet = `import { useBreakpoint } from '@backstage/ui';
-
-function ResponsiveComponent() {
- const { breakpoint, up, down } = useBreakpoint();
-
- return (
-
-
Current Breakpoint: {breakpoint}
- {up('md') &&
The viewport is medium or larger.
}
- {down('sm') &&
The viewport is small or smaller.
}
-
- );
-}`;
diff --git a/docs-ui/src/app/hooks/use-breakpoint/snippets.ts b/docs-ui/src/app/hooks/use-breakpoint/snippets.ts
new file mode 100644
index 0000000000..19e6debe65
--- /dev/null
+++ b/docs-ui/src/app/hooks/use-breakpoint/snippets.ts
@@ -0,0 +1,13 @@
+export const useBreakpointExampleSnippet = `import { useBreakpoint } from '@backstage/ui';
+
+function ResponsiveComponent() {
+ const { breakpoint, up, down } = useBreakpoint();
+
+ return (
+
+
Current Breakpoint: {breakpoint}
+ {up('md') &&
The viewport is medium or larger.
}
+ {down('sm') &&
The viewport is small or smaller.
}
+
+ );
+}`;
diff --git a/docs-ui/src/content/hooks/use-media-query.example.tsx b/docs-ui/src/app/hooks/use-media-query/components.tsx
similarity index 100%
rename from docs-ui/src/content/hooks/use-media-query.example.tsx
rename to docs-ui/src/app/hooks/use-media-query/components.tsx
diff --git a/docs-ui/src/content/hooks/use-media-query.mdx b/docs-ui/src/app/hooks/use-media-query/page.mdx
similarity index 95%
rename from docs-ui/src/content/hooks/use-media-query.mdx
rename to docs-ui/src/app/hooks/use-media-query/page.mdx
index 6941bc0d68..84668b2a3e 100644
--- a/docs-ui/src/content/hooks/use-media-query.mdx
+++ b/docs-ui/src/app/hooks/use-media-query/page.mdx
@@ -2,21 +2,23 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
import { CodeBlock } from '@/components/CodeBlock';
import { PageTitle } from '@/components/PageTitle';
import { PropsTable } from '@/components/PropsTable';
-import {
- useMediaQueryOrientationSnippet,
- useMediaQueryParamDefs,
- useMediaQueryPreferencesSnippet,
- useMediaQueryResponsiveSnippet,
- useMediaQueryReturnDefs,
- useMediaQueryUsageSnippet,
-} from './use-media-query.props';
import { Snippet } from '@/components/Snippet';
import {
- UseMediaQueryThemeExample,
+ UseMediaQueryOrientationExample,
UseMediaQueryPreferencesExample,
UseMediaQueryResponsiveExample,
- UseMediaQueryOrientationExample,
-} from './use-media-query.example';
+ UseMediaQueryThemeExample,
+} from './components';
+import {
+ useMediaQueryParamDefs,
+ useMediaQueryReturnDefs,
+} from './props-definition';
+import {
+ useMediaQueryOrientationSnippet,
+ useMediaQueryPreferencesSnippet,
+ useMediaQueryResponsiveSnippet,
+ useMediaQueryUsageSnippet,
+} from './snippets';
= {
+ query: {
+ type: 'string',
+ description: 'The CSS media query to evaluate',
+ },
+ options: {
+ type: 'complex',
+ complexType: {
+ name: 'UseMediaQueryOptions',
+ properties: {
+ defaultValue: {
+ type: 'boolean',
+ required: false,
+ description:
+ 'Default value to use when rendering on the server, defaults to false',
+ },
+ initializeWithValue: {
+ type: 'boolean',
+ required: false,
+ description:
+ 'Whether to initialize with the current value of the media query, or use the default value initially',
+ },
+ },
+ },
+ default: 'defaultValue: false\ninitializeWithValue: true',
+ },
+};
+
+export const useMediaQueryReturnDefs: Record = {
+ matches: {
+ type: 'boolean',
+ description: 'True if the media query currently matches',
+ },
+};
diff --git a/docs-ui/src/content/hooks/use-media-query.props.ts b/docs-ui/src/app/hooks/use-media-query/snippets.ts
similarity index 58%
rename from docs-ui/src/content/hooks/use-media-query.props.ts
rename to docs-ui/src/app/hooks/use-media-query/snippets.ts
index 4a3bb1ca73..78423ef516 100644
--- a/docs-ui/src/content/hooks/use-media-query.props.ts
+++ b/docs-ui/src/app/hooks/use-media-query/snippets.ts
@@ -1,40 +1,3 @@
-import { type PropDef } from '@/utils/propDefs';
-
-export const useMediaQueryParamDefs: Record = {
- query: {
- type: 'string',
- description: 'The CSS media query to evaluate',
- },
- options: {
- type: 'complex',
- complexType: {
- name: 'UseMediaQueryOptions',
- properties: {
- defaultValue: {
- type: 'boolean',
- required: false,
- description:
- 'Default value to use when rendering on the server, defaults to false',
- },
- initializeWithValue: {
- type: 'boolean',
- required: false,
- description:
- 'Whether to initialize with the current value of the media query, or use the default value initially',
- },
- },
- },
- default: 'defaultValue: false\ninitializeWithValue: true',
- },
-};
-
-export const useMediaQueryReturnDefs: Record = {
- matches: {
- type: 'boolean',
- description: 'True if the media query currently matches',
- },
-};
-
export const useMediaQueryUsageSnippet = `import { useMediaQuery } from '@backstage/ui';
function MyComponent() {
From 9c7b3dbf1cdf72829c8e340ed65819d828caf0bb Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Fri, 16 Jan 2026 18:36:03 +0100
Subject: [PATCH 03/14] chore: run prettier
Signed-off-by: Antony Bouyon
---
docs-ui/src/app/hooks/use-breakpoint/page.mdx | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/docs-ui/src/app/hooks/use-breakpoint/page.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
index 47ac3ab84f..4ee410fc74 100644
--- a/docs-ui/src/app/hooks/use-breakpoint/page.mdx
+++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
@@ -4,12 +4,8 @@ import { PageTitle } from '@/components/PageTitle';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { UseBreakpointExample } from './components';
-import {
- useBreakpointReturnDefs,
-} from './props-definition';
-import {
- useBreakpointExampleSnippet
-} from './snippets';
+import { useBreakpointReturnDefs } from './props-definition';
+import { useBreakpointExampleSnippet } from './snippets';
Date: Wed, 21 Jan 2026 13:16:08 +0100
Subject: [PATCH 04/14] fix: improve banner readability
Signed-off-by: Antony Bouyon
---
docs-ui/src/components/Banner/styles.module.css | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs-ui/src/components/Banner/styles.module.css b/docs-ui/src/components/Banner/styles.module.css
index 9666c5d521..d9d3323376 100644
--- a/docs-ui/src/components/Banner/styles.module.css
+++ b/docs-ui/src/components/Banner/styles.module.css
@@ -16,13 +16,13 @@
.info {
background-color: #f2f2f2;
border-color: #cdcdcd;
- color: #888888;
+ color: #4b4b4b;
}
.warning {
background-color: #fff2b9;
- border-color: #ffd000;
- color: #d79927;
+ border-color: #d79927;
+ color: #8a3d09;
}
.icon {
From a57041b196a0c5cd31dbeae15d0c851a4968b942 Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Wed, 21 Jan 2026 13:16:46 +0100
Subject: [PATCH 05/14] feat: add useStyles hook documentation & disclaimer for
internal hooks
Signed-off-by: Antony Bouyon
---
docs-ui/src/app/hooks/page.mdx | 12 +-
.../src/app/hooks/use-media-query/page.mdx | 6 +
docs-ui/src/app/hooks/use-styles/page.mdx | 59 ++++++++++
.../app/hooks/use-styles/props-definition.ts | 58 ++++++++++
docs-ui/src/app/hooks/use-styles/snippets.ts | 105 ++++++++++++++++++
5 files changed, 229 insertions(+), 11 deletions(-)
create mode 100644 docs-ui/src/app/hooks/use-styles/page.mdx
create mode 100644 docs-ui/src/app/hooks/use-styles/props-definition.ts
create mode 100644 docs-ui/src/app/hooks/use-styles/snippets.ts
diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx
index b2e2b81e7d..75bee70743 100644
--- a/docs-ui/src/app/hooks/page.mdx
+++ b/docs-ui/src/app/hooks/page.mdx
@@ -18,17 +18,7 @@ Backstage UI custom hooks provide easy access to design system features, style m
/>
-
-
diff --git a/docs-ui/src/app/hooks/use-media-query/page.mdx b/docs-ui/src/app/hooks/use-media-query/page.mdx
index 84668b2a3e..bd01152581 100644
--- a/docs-ui/src/app/hooks/use-media-query/page.mdx
+++ b/docs-ui/src/app/hooks/use-media-query/page.mdx
@@ -3,6 +3,7 @@ import { CodeBlock } from '@/components/CodeBlock';
import { PageTitle } from '@/components/PageTitle';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
+import { Banner } from '@/components/Banner';
import {
UseMediaQueryOrientationExample,
UseMediaQueryPreferencesExample,
@@ -25,6 +26,11 @@ import {
description="Hook to evaluate and react to CSS media queries programmatically."
/>
+
+
## Usage
The `useMediaQuery` hook allows you to evaluate CSS media queries in your components, enabling responsive design and behavior.
diff --git a/docs-ui/src/app/hooks/use-styles/page.mdx b/docs-ui/src/app/hooks/use-styles/page.mdx
new file mode 100644
index 0000000000..ca235c4778
--- /dev/null
+++ b/docs-ui/src/app/hooks/use-styles/page.mdx
@@ -0,0 +1,59 @@
+import { ChangelogComponent } from '@/components/ChangelogComponent';
+import { CodeBlock } from '@/components/CodeBlock';
+import { Banner } from '@/components/Banner';
+import { PageTitle } from '@/components/PageTitle';
+import { PropsTable } from '@/components/PropsTable';
+import {
+ useStylesUsageSnippet,
+ useStylesDefsSnippet,
+ useStylesAttributesSnippet,
+} from './snippets';
+import { useStylesPropsDefs, useStylesReturnDefs } from './props-definition';
+
+
+
+
+
+## Usage
+
+The `useStyles` hook is the core styling utility that processes component definitions and props to generate:
+
+- **Class names**
+- **Data attributes** for state-based styling (e.g., `data-disabled`, `data-variant`)
+- **Utility classes** from utility props like `padding`, `margin`, `display` (with responsive support)
+- **CSS custom properties** for custom values not in predefined utility lists
+- **Cleaned props** with utility props removed (safe to spread onto DOM elements)
+
+
+
+## Parameters
+
+
+## Return Value
+
+
+
+## How It Works
+
+### Component Definition
+
+A component definition describes how a component should be styled:
+
+
+
+
+
+### Data Attributes
+
+Data attributes enable state-based styling through CSS selectors. The hook automatically adds `data-*` to the attributes based on the defined props:
+
+
+
+
+
diff --git a/docs-ui/src/app/hooks/use-styles/props-definition.ts b/docs-ui/src/app/hooks/use-styles/props-definition.ts
new file mode 100644
index 0000000000..0c8a2a1728
--- /dev/null
+++ b/docs-ui/src/app/hooks/use-styles/props-definition.ts
@@ -0,0 +1,58 @@
+import { type PropDef } from '@/utils/propDefs';
+
+export const useStylesPropsDefs: Record = {
+ componentDefinition: {
+ type: 'complex',
+ complexType: {
+ name: 'ComponentDefinition',
+ properties: {
+ classNames: {
+ type: 'Record',
+ required: true,
+ },
+ dataAttributes: {
+ type: 'Record',
+ required: false,
+ },
+ utilityProps: {
+ type: 'string[]',
+ required: false,
+ },
+ },
+ },
+ },
+ props: {
+ type: 'enum',
+ description: 'All component props',
+ values: ['{ [key: string]: any; }'],
+ },
+};
+
+export const useStylesReturnDefs: Record = {
+ classNames: {
+ type: 'enum',
+ values: ['Record'],
+ description:
+ "The component's class names mapped to their style definitions",
+ },
+ dataAttributes: {
+ type: 'enum',
+ values: ['Record'],
+ description:
+ 'Data attributes generated from the component definition and props',
+ },
+ utilityClasses: {
+ type: 'string',
+ description: 'Combined utility classes based on utility props',
+ },
+ style: {
+ type: 'enum',
+ values: ['React.CSSProperties'],
+ description: 'The combined style object for the component',
+ },
+ cleanedProps: {
+ type: 'enum',
+ values: ['Record'],
+ description: 'The original props with utility props removed',
+ },
+};
diff --git a/docs-ui/src/app/hooks/use-styles/snippets.ts b/docs-ui/src/app/hooks/use-styles/snippets.ts
new file mode 100644
index 0000000000..55cf7ddee7
--- /dev/null
+++ b/docs-ui/src/app/hooks/use-styles/snippets.ts
@@ -0,0 +1,105 @@
+export const useStylesUsageSnippet = `import { useStyles } from '@backstage/ui';
+import type {
+ ComponentDefinition,
+ SpaceProps,
+ UtilityProps,
+} from '@backstage/ui';
+
+// Define your component's styling configuration
+const buttonDefinition: ComponentDefinition = {
+ classNames: {
+ root: 'bui-button',
+ icon: 'bui-button-icon',
+ },
+ dataAttributes: {
+ variant: ['primary', 'secondary', 'ghost'] as const,
+ size: ['small', 'medium', 'large'] as const,
+ 'is-disabled': [true, false] as const,
+ },
+ utilityProps: ['gap', 'mb', 'mt', 'ml', 'mr'],
+} as const satisfies ComponentDefinition;
+
+// type the component props
+interface ButtonProps {
+ variant?: 'primary' | 'secondary' | 'ghost';
+ size?: 'small' | 'medium' | 'large';
+ isDisabled?: boolean;
+ gap?: UtilityProps['gap'];
+ mb?: SpaceProps['mb'];
+ mt?: SpaceProps['mt'];
+ ml?: SpaceProps['ml'];
+ mr?: SpaceProps['mr'];
+ icon?: React.ReactElement;
+ children?: React.ReactNode;
+}
+
+// Use the useStyles hook in your component
+export function Button({
+ variant = 'primary',
+ size = 'medium',
+ isDisabled,
+ ...props
+}: Readonly) {
+ const { classNames, dataAttributes, utilityClasses, cleanedProps } =
+ useStyles(buttonDefinition, {
+ variant,
+ size,
+ 'is-disabled': isDisabled,
+ ...props,
+ });
+
+ return (
+
+ );
+}`;
+
+export const useStylesDefsSnippet = `const componentDefinition = {
+ // CSS class names for component parts
+ classNames: {
+ root: 'bui-button',
+ icon: 'bui-button-icon',
+ },
+
+ // Props that become data attributes for CSS targeting
+ // data-attribute must be kebab-case to respect HTML standards
+ // And casing matters here, lowercase only:
+ // 'variant' not 'Variant', 'is-disabled' not 'isDisabled'..
+ dataAttributes: {
+ variant: ['primary', 'secondary'] as const,
+ size: ['small', 'large'] as const,
+ 'is-disabled': [true, false] as const,
+ },
+
+ // Props that become utility classes (spacing, layout, etc.)
+ utilityProps: ['m', 'mb', 'mt', 'ml', 'mr'],
+};`;
+
+export const useStylesAttributesSnippet = `/* Component CSS can use data attributes for variants */
+.bui-button {
+ padding: 0.5rem 1rem;
+ border-radius: 0.25rem;
+}
+
+/* Variant styles */
+.bui-button[data-variant="primary"] {
+ background: blue;
+ color: white;
+}
+
+.bui-button[data-variant="secondary"] {
+ background: gray;
+ color: black;
+}
+
+/* State styles */
+.bui-button[data-is-disabled="true"] {
+ opacity: 0.5;
+ pointer-events: none;
+}`;
From 8317533ea978b00b86f9e633c108a614f4f24164 Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Wed, 21 Jan 2026 13:49:16 +0100
Subject: [PATCH 06/14] fix: improve useMediaQuery examples
Signed-off-by: Antony Bouyon
---
.../app/hooks/use-media-query/components.tsx | 15 +++++------
.../src/app/hooks/use-media-query/example.css | 25 +++++++++++++++++++
.../src/app/hooks/use-media-query/snippets.ts | 23 +++++++++--------
docs-ui/src/app/hooks/use-styles/page.mdx | 7 ++++--
4 files changed, 50 insertions(+), 20 deletions(-)
create mode 100644 docs-ui/src/app/hooks/use-media-query/example.css
diff --git a/docs-ui/src/app/hooks/use-media-query/components.tsx b/docs-ui/src/app/hooks/use-media-query/components.tsx
index 0d8edd944b..1ebcd64dfa 100644
--- a/docs-ui/src/app/hooks/use-media-query/components.tsx
+++ b/docs-ui/src/app/hooks/use-media-query/components.tsx
@@ -1,13 +1,12 @@
'use client';
import { useMediaQuery } from '@backstage/ui/src/hooks/useMediaQuery';
+import './example.css';
export function UseMediaQueryThemeExample() {
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
return (
-
- {isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'}
-
+ {isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'}
);
}
@@ -17,11 +16,11 @@ export function UseMediaQueryResponsiveExample() {
const isDesktop = useMediaQuery('(min-width: 1025px)');
return (
-
+
{isMobile && ''}
{isTablet && ''}
{isDesktop && ''}
-
+
);
}
@@ -31,12 +30,14 @@ export function UseMediaQueryPreferencesExample() {
);
return (
- Content
+
+ Content
+
);
}
export function UseMediaQueryOrientationExample() {
const isPortrait = useMediaQuery('(orientation: portrait)');
- return {isPortrait ? 'Portrait mode' : 'Landscape mode'}
;
+ return {isPortrait ? 'Portrait mode' : 'Landscape mode'}
;
}
diff --git a/docs-ui/src/app/hooks/use-media-query/example.css b/docs-ui/src/app/hooks/use-media-query/example.css
new file mode 100644
index 0000000000..5fb132f9f4
--- /dev/null
+++ b/docs-ui/src/app/hooks/use-media-query/example.css
@@ -0,0 +1,25 @@
+@keyframes borderSlide {
+ 0%,
+ 100% {
+ transform: translateX(-90%);
+ }
+ 50% {
+ transform: translateX(190%);
+ }
+}
+
+.animated-border {
+ position: relative;
+ overflow: hidden;
+}
+
+.animated-border::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 50%;
+ height: 2px;
+ background-color: currentColor;
+ animation: borderSlide 5s ease-in-out infinite;
+}
diff --git a/docs-ui/src/app/hooks/use-media-query/snippets.ts b/docs-ui/src/app/hooks/use-media-query/snippets.ts
index 78423ef516..157da945f0 100644
--- a/docs-ui/src/app/hooks/use-media-query/snippets.ts
+++ b/docs-ui/src/app/hooks/use-media-query/snippets.ts
@@ -4,9 +4,12 @@ function MyComponent() {
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
return (
-
- {isDarkMode ? 'Dark mode enabled' : 'Light mode enabled'}
-
+
+ {isDarkMode
+ ? 'User prefers Dark mode'
+ : 'User prefers Light mode'
+ }
+
);
}`;
@@ -18,11 +21,11 @@ function ResponsiveLayout() {
const isDesktop = useMediaQuery('(min-width: 1025px)');
return (
-
+
{isMobile && }
{isTablet && }
{isDesktop && }
-
+
);
}`;
@@ -32,11 +35,9 @@ function AccessibleComponent() {
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
return (
-
+
);
}`;
@@ -46,8 +47,8 @@ function OrientationAware() {
const isPortrait = useMediaQuery('(orientation: portrait)');
return (
-
+
{isPortrait ? 'Portrait mode' : 'Landscape mode'}
-
+
);
}`;
diff --git a/docs-ui/src/app/hooks/use-styles/page.mdx b/docs-ui/src/app/hooks/use-styles/page.mdx
index ca235c4778..080a283dcc 100644
--- a/docs-ui/src/app/hooks/use-styles/page.mdx
+++ b/docs-ui/src/app/hooks/use-styles/page.mdx
@@ -33,6 +33,7 @@ The `useStyles` hook is the core styling utility that processes component defini
## Parameters
+
## Return Value
@@ -47,7 +48,10 @@ A component definition describes how a component should be styled:
-
+
### Data Attributes
@@ -55,5 +59,4 @@ Data attributes enable state-based styling through CSS selectors. The hook autom
-
From d682eef1173332a8f80987cdcc5b2abf09ef55bf Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Wed, 21 Jan 2026 14:15:17 +0100
Subject: [PATCH 07/14] chore: remove undocumented hooks from utils/data.ts
Signed-off-by: Antony Bouyon
---
docs-ui/src/utils/data.ts | 8 --------
1 file changed, 8 deletions(-)
diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts
index bb1ea40dfb..a750f75bd6 100644
--- a/docs-ui/src/utils/data.ts
+++ b/docs-ui/src/utils/data.ts
@@ -144,10 +144,6 @@ export const hooks: Page[] = [
title: 'useBreakpoint',
slug: 'use-breakpoint',
},
- {
- title: 'useIsomorphicLayoutEffect',
- slug: 'use-isomorphic-layout-effect',
- },
{
title: 'useMediaQuery',
slug: 'use-media-query',
@@ -156,8 +152,4 @@ export const hooks: Page[] = [
title: 'useStyles',
slug: 'use-styles',
},
- {
- title: 'useSurface',
- slug: 'use-surface',
- },
];
From 50cdc709206534e73ef966cdebb87e7b94de6bf5 Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Thu, 22 Jan 2026 10:10:33 +0100
Subject: [PATCH 08/14] fix: wrong logic operator
Signed-off-by: Antony Bouyon
---
docs-ui/src/app/hooks/use-styles/snippets.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs-ui/src/app/hooks/use-styles/snippets.ts b/docs-ui/src/app/hooks/use-styles/snippets.ts
index 55cf7ddee7..60c5a66a9b 100644
--- a/docs-ui/src/app/hooks/use-styles/snippets.ts
+++ b/docs-ui/src/app/hooks/use-styles/snippets.ts
@@ -54,7 +54,7 @@ export function Button({
{...dataAttributes}
{...cleanedProps}
>
- {props.icon ?? {props.icon}}
+ {props.icon && {props.icon}}
{props.children}
);
From 6325d6b307d889d7884858ed13c1b0fea81117ce Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Tue, 3 Feb 2026 12:49:22 +0100
Subject: [PATCH 09/14] feat: remove internal hooks & add performance
disclaimer to useBreakpoint
Signed-off-by: Antony Bouyon
---
docs-ui/src/app/hooks/page.mdx | 12 +-
docs-ui/src/app/hooks/use-breakpoint/page.mdx | 8 +-
.../app/hooks/use-media-query/components.tsx | 43 -------
.../src/app/hooks/use-media-query/example.css | 25 -----
.../src/app/hooks/use-media-query/page.mdx | 95 ----------------
.../hooks/use-media-query/props-definition.ts | 36 ------
.../src/app/hooks/use-media-query/snippets.ts | 54 ---------
docs-ui/src/app/hooks/use-styles/page.mdx | 62 -----------
.../app/hooks/use-styles/props-definition.ts | 58 ----------
docs-ui/src/app/hooks/use-styles/snippets.ts | 105 ------------------
docs-ui/src/utils/data.ts | 8 --
11 files changed, 8 insertions(+), 498 deletions(-)
delete mode 100644 docs-ui/src/app/hooks/use-media-query/components.tsx
delete mode 100644 docs-ui/src/app/hooks/use-media-query/example.css
delete mode 100644 docs-ui/src/app/hooks/use-media-query/page.mdx
delete mode 100644 docs-ui/src/app/hooks/use-media-query/props-definition.ts
delete mode 100644 docs-ui/src/app/hooks/use-media-query/snippets.ts
delete mode 100644 docs-ui/src/app/hooks/use-styles/page.mdx
delete mode 100644 docs-ui/src/app/hooks/use-styles/props-definition.ts
delete mode 100644 docs-ui/src/app/hooks/use-styles/snippets.ts
diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx
index 75bee70743..f4b77c3a0c 100644
--- a/docs-ui/src/app/hooks/page.mdx
+++ b/docs-ui/src/app/hooks/page.mdx
@@ -8,17 +8,7 @@ Backstage UI custom hooks provide easy access to design system features, style m
-
-
diff --git a/docs-ui/src/app/hooks/use-breakpoint/page.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
index 4ee410fc74..a5c1a64e92 100644
--- a/docs-ui/src/app/hooks/use-breakpoint/page.mdx
+++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
@@ -3,6 +3,7 @@ import { CodeBlock } from '@/components/CodeBlock';
import { PageTitle } from '@/components/PageTitle';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
+import { Banner } from '@/components/Banner';
import { UseBreakpointExample } from './components';
import { useBreakpointReturnDefs } from './props-definition';
import { useBreakpointExampleSnippet } from './snippets';
@@ -14,6 +15,11 @@ import { useBreakpointExampleSnippet } from './snippets';
## Usage
+
+
The `useBreakpoint` hook returns the active breakpoint and two functions, `up` and `down`, to check if the viewport is above, or below a given breakpoint, letting you adjust your UI responsively.
-
+
diff --git a/docs-ui/src/app/hooks/use-media-query/components.tsx b/docs-ui/src/app/hooks/use-media-query/components.tsx
deleted file mode 100644
index 1ebcd64dfa..0000000000
--- a/docs-ui/src/app/hooks/use-media-query/components.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-'use client';
-import { useMediaQuery } from '@backstage/ui/src/hooks/useMediaQuery';
-import './example.css';
-
-export function UseMediaQueryThemeExample() {
- const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
-
- return (
- {isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'}
- );
-}
-
-export function UseMediaQueryResponsiveExample() {
- const isMobile = useMediaQuery('(max-width: 768px)');
- const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
- const isDesktop = useMediaQuery('(min-width: 1025px)');
-
- return (
-
- {isMobile && ''}
- {isTablet && ''}
- {isDesktop && ''}
-
- );
-}
-
-export function UseMediaQueryPreferencesExample() {
- const prefersReducedMotion = useMediaQuery(
- '(prefers-reduced-motion: reduce)',
- );
-
- return (
-
- Content
-
- );
-}
-
-export function UseMediaQueryOrientationExample() {
- const isPortrait = useMediaQuery('(orientation: portrait)');
-
- return {isPortrait ? 'Portrait mode' : 'Landscape mode'}
;
-}
diff --git a/docs-ui/src/app/hooks/use-media-query/example.css b/docs-ui/src/app/hooks/use-media-query/example.css
deleted file mode 100644
index 5fb132f9f4..0000000000
--- a/docs-ui/src/app/hooks/use-media-query/example.css
+++ /dev/null
@@ -1,25 +0,0 @@
-@keyframes borderSlide {
- 0%,
- 100% {
- transform: translateX(-90%);
- }
- 50% {
- transform: translateX(190%);
- }
-}
-
-.animated-border {
- position: relative;
- overflow: hidden;
-}
-
-.animated-border::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 0;
- width: 50%;
- height: 2px;
- background-color: currentColor;
- animation: borderSlide 5s ease-in-out infinite;
-}
diff --git a/docs-ui/src/app/hooks/use-media-query/page.mdx b/docs-ui/src/app/hooks/use-media-query/page.mdx
deleted file mode 100644
index bd01152581..0000000000
--- a/docs-ui/src/app/hooks/use-media-query/page.mdx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { CodeBlock } from '@/components/CodeBlock';
-import { PageTitle } from '@/components/PageTitle';
-import { PropsTable } from '@/components/PropsTable';
-import { Snippet } from '@/components/Snippet';
-import { Banner } from '@/components/Banner';
-import {
- UseMediaQueryOrientationExample,
- UseMediaQueryPreferencesExample,
- UseMediaQueryResponsiveExample,
- UseMediaQueryThemeExample,
-} from './components';
-import {
- useMediaQueryParamDefs,
- useMediaQueryReturnDefs,
-} from './props-definition';
-import {
- useMediaQueryOrientationSnippet,
- useMediaQueryPreferencesSnippet,
- useMediaQueryResponsiveSnippet,
- useMediaQueryUsageSnippet,
-} from './snippets';
-
-
-
-
-
-## Usage
-
-The `useMediaQuery` hook allows you to evaluate CSS media queries in your components, enabling responsive design and behavior.
-
-}
- code={useMediaQueryUsageSnippet}
-/>
-
-## Parameters
-
-
-
-## Return Value
-
-
-## Examples
-
-### Responsive Layouts
-
-Adapt layout based on different screen sizes.
-
-}
- code={useMediaQueryResponsiveSnippet}
-/>
-
-### User Preferences
-
-Respect user system preferences.
-
-}
- code={useMediaQueryPreferencesSnippet}
-/>
-
-### Device Orientation
-
-Detect screen orientation.
-
-}
- code={useMediaQueryOrientationSnippet}
-/>
-
-
diff --git a/docs-ui/src/app/hooks/use-media-query/props-definition.ts b/docs-ui/src/app/hooks/use-media-query/props-definition.ts
deleted file mode 100644
index 315696e74a..0000000000
--- a/docs-ui/src/app/hooks/use-media-query/props-definition.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { type PropDef } from '@/utils/propDefs';
-
-export const useMediaQueryParamDefs: Record = {
- query: {
- type: 'string',
- description: 'The CSS media query to evaluate',
- },
- options: {
- type: 'complex',
- complexType: {
- name: 'UseMediaQueryOptions',
- properties: {
- defaultValue: {
- type: 'boolean',
- required: false,
- description:
- 'Default value to use when rendering on the server, defaults to false',
- },
- initializeWithValue: {
- type: 'boolean',
- required: false,
- description:
- 'Whether to initialize with the current value of the media query, or use the default value initially',
- },
- },
- },
- default: 'defaultValue: false\ninitializeWithValue: true',
- },
-};
-
-export const useMediaQueryReturnDefs: Record = {
- matches: {
- type: 'boolean',
- description: 'True if the media query currently matches',
- },
-};
diff --git a/docs-ui/src/app/hooks/use-media-query/snippets.ts b/docs-ui/src/app/hooks/use-media-query/snippets.ts
deleted file mode 100644
index 157da945f0..0000000000
--- a/docs-ui/src/app/hooks/use-media-query/snippets.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-export const useMediaQueryUsageSnippet = `import { useMediaQuery } from '@backstage/ui';
-
-function MyComponent() {
- const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
-
- return (
-
- {isDarkMode
- ? 'User prefers Dark mode'
- : 'User prefers Light mode'
- }
-
- );
-}`;
-
-export const useMediaQueryResponsiveSnippet = `import { useMediaQuery } from '@backstage/ui';
-
-function ResponsiveLayout() {
- const isMobile = useMediaQuery('(max-width: 768px)');
- const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
- const isDesktop = useMediaQuery('(min-width: 1025px)');
-
- return (
-
- {isMobile && }
- {isTablet && }
- {isDesktop && }
-
- );
-}`;
-
-export const useMediaQueryPreferencesSnippet = `import { useMediaQuery } from '@backstage/ui';
-
-function AccessibleComponent() {
- const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
-
- return (
-
- Content
-
- );
-}`;
-
-export const useMediaQueryOrientationSnippet = `import { useMediaQuery } from '@backstage/ui';
-
-function OrientationAware() {
- const isPortrait = useMediaQuery('(orientation: portrait)');
-
- return (
-
- {isPortrait ? 'Portrait mode' : 'Landscape mode'}
-
- );
-}`;
diff --git a/docs-ui/src/app/hooks/use-styles/page.mdx b/docs-ui/src/app/hooks/use-styles/page.mdx
deleted file mode 100644
index 080a283dcc..0000000000
--- a/docs-ui/src/app/hooks/use-styles/page.mdx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { CodeBlock } from '@/components/CodeBlock';
-import { Banner } from '@/components/Banner';
-import { PageTitle } from '@/components/PageTitle';
-import { PropsTable } from '@/components/PropsTable';
-import {
- useStylesUsageSnippet,
- useStylesDefsSnippet,
- useStylesAttributesSnippet,
-} from './snippets';
-import { useStylesPropsDefs, useStylesReturnDefs } from './props-definition';
-
-
-
-
-
-## Usage
-
-The `useStyles` hook is the core styling utility that processes component definitions and props to generate:
-
-- **Class names**
-- **Data attributes** for state-based styling (e.g., `data-disabled`, `data-variant`)
-- **Utility classes** from utility props like `padding`, `margin`, `display` (with responsive support)
-- **CSS custom properties** for custom values not in predefined utility lists
-- **Cleaned props** with utility props removed (safe to spread onto DOM elements)
-
-
-
-## Parameters
-
-
-
-## Return Value
-
-
-
-## How It Works
-
-### Component Definition
-
-A component definition describes how a component should be styled:
-
-
-
-
-
-### Data Attributes
-
-Data attributes enable state-based styling through CSS selectors. The hook automatically adds `data-*` to the attributes based on the defined props:
-
-
-
-
diff --git a/docs-ui/src/app/hooks/use-styles/props-definition.ts b/docs-ui/src/app/hooks/use-styles/props-definition.ts
deleted file mode 100644
index 0c8a2a1728..0000000000
--- a/docs-ui/src/app/hooks/use-styles/props-definition.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { type PropDef } from '@/utils/propDefs';
-
-export const useStylesPropsDefs: Record = {
- componentDefinition: {
- type: 'complex',
- complexType: {
- name: 'ComponentDefinition',
- properties: {
- classNames: {
- type: 'Record',
- required: true,
- },
- dataAttributes: {
- type: 'Record',
- required: false,
- },
- utilityProps: {
- type: 'string[]',
- required: false,
- },
- },
- },
- },
- props: {
- type: 'enum',
- description: 'All component props',
- values: ['{ [key: string]: any; }'],
- },
-};
-
-export const useStylesReturnDefs: Record = {
- classNames: {
- type: 'enum',
- values: ['Record'],
- description:
- "The component's class names mapped to their style definitions",
- },
- dataAttributes: {
- type: 'enum',
- values: ['Record'],
- description:
- 'Data attributes generated from the component definition and props',
- },
- utilityClasses: {
- type: 'string',
- description: 'Combined utility classes based on utility props',
- },
- style: {
- type: 'enum',
- values: ['React.CSSProperties'],
- description: 'The combined style object for the component',
- },
- cleanedProps: {
- type: 'enum',
- values: ['Record'],
- description: 'The original props with utility props removed',
- },
-};
diff --git a/docs-ui/src/app/hooks/use-styles/snippets.ts b/docs-ui/src/app/hooks/use-styles/snippets.ts
deleted file mode 100644
index 60c5a66a9b..0000000000
--- a/docs-ui/src/app/hooks/use-styles/snippets.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-export const useStylesUsageSnippet = `import { useStyles } from '@backstage/ui';
-import type {
- ComponentDefinition,
- SpaceProps,
- UtilityProps,
-} from '@backstage/ui';
-
-// Define your component's styling configuration
-const buttonDefinition: ComponentDefinition = {
- classNames: {
- root: 'bui-button',
- icon: 'bui-button-icon',
- },
- dataAttributes: {
- variant: ['primary', 'secondary', 'ghost'] as const,
- size: ['small', 'medium', 'large'] as const,
- 'is-disabled': [true, false] as const,
- },
- utilityProps: ['gap', 'mb', 'mt', 'ml', 'mr'],
-} as const satisfies ComponentDefinition;
-
-// type the component props
-interface ButtonProps {
- variant?: 'primary' | 'secondary' | 'ghost';
- size?: 'small' | 'medium' | 'large';
- isDisabled?: boolean;
- gap?: UtilityProps['gap'];
- mb?: SpaceProps['mb'];
- mt?: SpaceProps['mt'];
- ml?: SpaceProps['ml'];
- mr?: SpaceProps['mr'];
- icon?: React.ReactElement;
- children?: React.ReactNode;
-}
-
-// Use the useStyles hook in your component
-export function Button({
- variant = 'primary',
- size = 'medium',
- isDisabled,
- ...props
-}: Readonly) {
- const { classNames, dataAttributes, utilityClasses, cleanedProps } =
- useStyles(buttonDefinition, {
- variant,
- size,
- 'is-disabled': isDisabled,
- ...props,
- });
-
- return (
-
- );
-}`;
-
-export const useStylesDefsSnippet = `const componentDefinition = {
- // CSS class names for component parts
- classNames: {
- root: 'bui-button',
- icon: 'bui-button-icon',
- },
-
- // Props that become data attributes for CSS targeting
- // data-attribute must be kebab-case to respect HTML standards
- // And casing matters here, lowercase only:
- // 'variant' not 'Variant', 'is-disabled' not 'isDisabled'..
- dataAttributes: {
- variant: ['primary', 'secondary'] as const,
- size: ['small', 'large'] as const,
- 'is-disabled': [true, false] as const,
- },
-
- // Props that become utility classes (spacing, layout, etc.)
- utilityProps: ['m', 'mb', 'mt', 'ml', 'mr'],
-};`;
-
-export const useStylesAttributesSnippet = `/* Component CSS can use data attributes for variants */
-.bui-button {
- padding: 0.5rem 1rem;
- border-radius: 0.25rem;
-}
-
-/* Variant styles */
-.bui-button[data-variant="primary"] {
- background: blue;
- color: white;
-}
-
-.bui-button[data-variant="secondary"] {
- background: gray;
- color: black;
-}
-
-/* State styles */
-.bui-button[data-is-disabled="true"] {
- opacity: 0.5;
- pointer-events: none;
-}`;
diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts
index a750f75bd6..a1a6672135 100644
--- a/docs-ui/src/utils/data.ts
+++ b/docs-ui/src/utils/data.ts
@@ -144,12 +144,4 @@ export const hooks: Page[] = [
title: 'useBreakpoint',
slug: 'use-breakpoint',
},
- {
- title: 'useMediaQuery',
- slug: 'use-media-query',
- },
- {
- title: 'useStyles',
- slug: 'use-styles',
- },
];
From 5b84e40b1bdcd0774fab92d8767010e5530114fc Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Tue, 3 Feb 2026 13:04:02 +0100
Subject: [PATCH 10/14] feat: add support for hooks in changelogs
Signed-off-by: Antony Bouyon
---
.../src/components/ChangelogComponent/index.tsx | 14 ++++++++++----
docs-ui/src/utils/types.ts | 14 +++++++++++---
2 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/docs-ui/src/components/ChangelogComponent/index.tsx b/docs-ui/src/components/ChangelogComponent/index.tsx
index 92068d04f8..1ba5c45a0b 100644
--- a/docs-ui/src/components/ChangelogComponent/index.tsx
+++ b/docs-ui/src/components/ChangelogComponent/index.tsx
@@ -1,16 +1,22 @@
import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
-import type { Component } from '@/utils/changelog';
+import type { Component, Hook } from '@/utils/changelog';
import {
Badge,
BreakingBadge,
generateChangelogMarkdown,
} from '../Changelog/utils';
-export const ChangelogComponent = ({ component }: { component: Component }) => {
- const componentChangelog = changelog.filter(c =>
- c.components.includes(component),
+export const ChangelogComponent = ({
+ component,
+ hook,
+}: {
+ component: Component;
+ hook: Hook;
+}) => {
+ const componentChangelog = changelog.filter(
+ c => c.components?.includes(component) || c.hooks?.includes(hook),
);
const content = `## Changelog
diff --git a/docs-ui/src/utils/types.ts b/docs-ui/src/utils/types.ts
index 21acc950a0..4fa0d8b1c3 100644
--- a/docs-ui/src/utils/types.ts
+++ b/docs-ui/src/utils/types.ts
@@ -35,14 +35,22 @@ export type Component =
| 'tooltip'
| 'visually-hidden';
+export type Hook = 'use-breakpoint';
+
export type Version = `${number}.${number}.${number}`;
-export interface ChangelogProps {
- components: Component[];
+type AtLeastOne = K extends string
+ ? Pick & Partial>
+ : never;
+
+export type ChangelogProps = {
description: string;
version: Version;
prs: string[];
breaking?: boolean;
commitSha?: string;
migration?: string;
-}
+} & AtLeastOne<{
+ components: Component[];
+ hooks: Hook[];
+}>;
From 1cd738e636fce041aef19d21cb0e8b3fc0e73500 Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Tue, 3 Feb 2026 13:34:21 +0100
Subject: [PATCH 11/14] fix: changelog expects component and/or hook
Signed-off-by: Antony Bouyon
---
docs-ui/src/components/ChangelogComponent/index.tsx | 12 +++++++-----
docs-ui/src/utils/types.ts | 2 +-
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/docs-ui/src/components/ChangelogComponent/index.tsx b/docs-ui/src/components/ChangelogComponent/index.tsx
index 1ba5c45a0b..cca75a9515 100644
--- a/docs-ui/src/components/ChangelogComponent/index.tsx
+++ b/docs-ui/src/components/ChangelogComponent/index.tsx
@@ -1,20 +1,22 @@
import { changelog } from '@/utils/changelog';
import { MDXRemote } from 'next-mdx-remote-client/rsc';
import { formattedMDXComponents } from '@/mdx-components';
-import type { Component, Hook } from '@/utils/changelog';
+import type { AtLeastOne, Component, Hook } from '@/utils/changelog';
import {
Badge,
BreakingBadge,
generateChangelogMarkdown,
} from '../Changelog/utils';
+type ChangelogComponentProps = AtLeastOne<{
+ component: Component;
+ hook: Hook;
+}>;
+
export const ChangelogComponent = ({
component,
hook,
-}: {
- component: Component;
- hook: Hook;
-}) => {
+}: Readonly) => {
const componentChangelog = changelog.filter(
c => c.components?.includes(component) || c.hooks?.includes(hook),
);
diff --git a/docs-ui/src/utils/types.ts b/docs-ui/src/utils/types.ts
index 4fa0d8b1c3..512609d1ea 100644
--- a/docs-ui/src/utils/types.ts
+++ b/docs-ui/src/utils/types.ts
@@ -39,7 +39,7 @@ export type Hook = 'use-breakpoint';
export type Version = `${number}.${number}.${number}`;
-type AtLeastOne = K extends string
+export type AtLeastOne = K extends string
? Pick & Partial>
: never;
From 15da6ee3597cbac44e5601edc8714796f315ca23 Mon Sep 17 00:00:00 2001
From: Antony
Date: Thu, 12 Feb 2026 11:59:56 +0100
Subject: [PATCH 12/14] Update docs-ui/src/app/hooks/page.mdx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Antony
---
docs-ui/src/app/hooks/page.mdx | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx
index f4b77c3a0c..e6fbde7b1a 100644
--- a/docs-ui/src/app/hooks/page.mdx
+++ b/docs-ui/src/app/hooks/page.mdx
@@ -1,4 +1,3 @@
-import { CodeBlock } from '@/components/CodeBlock';
import { ComponentCard, ComponentCards } from '@/components/ComponentCards';
# Hooks
From 0022eb1e0ab2e1a21172282da490be4fb83ece26 Mon Sep 17 00:00:00 2001
From: Antony
Date: Thu, 12 Feb 2026 12:00:06 +0100
Subject: [PATCH 13/14] Update docs-ui/src/app/hooks/use-breakpoint/page.mdx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Antony
---
docs-ui/src/app/hooks/use-breakpoint/page.mdx | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs-ui/src/app/hooks/use-breakpoint/page.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
index a5c1a64e92..5a077a3d73 100644
--- a/docs-ui/src/app/hooks/use-breakpoint/page.mdx
+++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
@@ -1,5 +1,4 @@
import { ChangelogComponent } from '@/components/ChangelogComponent';
-import { CodeBlock } from '@/components/CodeBlock';
import { PageTitle } from '@/components/PageTitle';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
From 175176729d4a141f07814d4e942bff0f66556c89 Mon Sep 17 00:00:00 2001
From: Antony Bouyon
Date: Thu, 12 Feb 2026 17:24:29 +0100
Subject: [PATCH 14/14] fix: hopefully fix everything that was broken/changed
Signed-off-by: Antony Bouyon
---
docs-ui/src/app/hooks/page.mdx | 10 ++----
.../app/hooks/use-breakpoint/components.tsx | 11 ------
docs-ui/src/app/hooks/use-breakpoint/page.mdx | 2 +-
.../components/HookGrid/HookGrid.module.css | 29 +++++++++++++++
docs-ui/src/components/HookGrid/HookGrid.tsx | 21 +++++++++++
docs-ui/src/components/HookGrid/index.ts | 1 +
.../src/components/Navigation/Navigation.tsx | 36 ++++++++++++-------
7 files changed, 77 insertions(+), 33 deletions(-)
create mode 100644 docs-ui/src/components/HookGrid/HookGrid.module.css
create mode 100644 docs-ui/src/components/HookGrid/HookGrid.tsx
create mode 100644 docs-ui/src/components/HookGrid/index.ts
diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx
index e6fbde7b1a..ea73f07ce3 100644
--- a/docs-ui/src/app/hooks/page.mdx
+++ b/docs-ui/src/app/hooks/page.mdx
@@ -1,13 +1,7 @@
-import { ComponentCard, ComponentCards } from '@/components/ComponentCards';
+import { HookGrid } from '@/components/HookGrid';
# Hooks
Backstage UI custom hooks provide easy access to design system features, style management, and responsive interface creation.
-
-
-
+
diff --git a/docs-ui/src/app/hooks/use-breakpoint/components.tsx b/docs-ui/src/app/hooks/use-breakpoint/components.tsx
index d4d21861bb..c58010c66f 100644
--- a/docs-ui/src/app/hooks/use-breakpoint/components.tsx
+++ b/docs-ui/src/app/hooks/use-breakpoint/components.tsx
@@ -1,20 +1,9 @@
'use client';
import { useBreakpoint } from '@backstage/ui';
-import { useEffect, useState } from 'react';
export function UseBreakpointExample() {
const { breakpoint, up, down } = useBreakpoint();
- const [isMounted, setIsMounted] = useState(false);
-
- // prevent hydration mismatch by rendering only on the client
- useEffect(() => {
- setIsMounted(true);
- }, []);
-
- if (!isMounted) {
- return null;
- }
return (
diff --git a/docs-ui/src/app/hooks/use-breakpoint/page.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
index 5a077a3d73..f7f3378a41 100644
--- a/docs-ui/src/app/hooks/use-breakpoint/page.mdx
+++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx
@@ -3,9 +3,9 @@ import { PageTitle } from '@/components/PageTitle';
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { Banner } from '@/components/Banner';
-import { UseBreakpointExample } from './components';
import { useBreakpointReturnDefs } from './props-definition';
import { useBreakpointExampleSnippet } from './snippets';
+import { UseBreakpointExample } from './components';
{
+ return (
+
+ {hooks.map(item => (
+
+ {item.title}
+
+ ))}
+
+ );
+};
diff --git a/docs-ui/src/components/HookGrid/index.ts b/docs-ui/src/components/HookGrid/index.ts
new file mode 100644
index 0000000000..934f73aacf
--- /dev/null
+++ b/docs-ui/src/components/HookGrid/index.ts
@@ -0,0 +1 @@
+export * from './HookGrid';
diff --git a/docs-ui/src/components/Navigation/Navigation.tsx b/docs-ui/src/components/Navigation/Navigation.tsx
index f4fc51993f..ac62c03d7a 100644
--- a/docs-ui/src/components/Navigation/Navigation.tsx
+++ b/docs-ui/src/components/Navigation/Navigation.tsx
@@ -18,19 +18,6 @@ interface NavigationProps {
onLinkClick?: () => void;
}
-const data = [
- {
- title: 'Components',
- content: components,
- url: '/components',
- },
- {
- title: 'Hooks',
- content: hooks,
- url: '/hooks',
- },
-];
-
export const Navigation = ({ onLinkClick }: NavigationProps) => {
const pathname = usePathname();
@@ -105,6 +92,29 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => {
);
})}
+
+ Hooks
+
+ {hooks.map(item => {
+ const isActive = pathname === `/hooks/${item.slug}`;
+ return (
+
+ {item.title}
+
+ {item.status === 'alpha' && 'Alpha'}
+ {item.status === 'beta' && 'Beta'}
+ {item.status === 'inProgress' && 'In Progress'}
+ {item.status === 'stable' && 'Stable'}
+ {item.status === 'deprecated' && 'Deprecated'}
+
+
+ );
+ })}
>
);
};