feat: remove internal hooks & add performance disclaimer to useBreakpoint

Signed-off-by: Antony Bouyon <antony.bouyon@believe.com>
This commit is contained in:
Antony Bouyon
2026-02-03 12:49:22 +01:00
parent 50cdc70920
commit 6325d6b307
11 changed files with 8 additions and 498 deletions
+1 -11
View File
@@ -8,17 +8,7 @@ Backstage UI custom hooks provide easy access to design system features, style m
<ComponentCards>
<ComponentCard
title="useBreakpoint"
description="Hook to detect current responsive breakpoints and manage adaptive behaviors."
description="Responsive breakpoint detection hook (performance optimizations in progress)."
href="/hooks/use-breakpoint"
/>
<ComponentCard
title="useMediaQuery"
description="Hook to evaluate and react to CSS media queries programmatically."
href="/hooks/use-media-query"
/>
<ComponentCard
title="useStyles"
description="Main hook to access and manipulate design system styles."
href="/hooks/use-styles"
/>
</ComponentCards>
@@ -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
<Banner
variant="warning"
text="This hook has performance issues and should be used with caution."
/>
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.
<Snippet
@@ -39,4 +45,4 @@ The default breakpoints are:
<PropsTable data={useBreakpointReturnDefs} isHook />
<ChangelogComponent component="use-breakpoint" />
<ChangelogComponent hook="use-breakpoint" />
@@ -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 (
<p>{isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'}</p>
);
}
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 (
<p>
{isMobile && '<MobileLayout />'}
{isTablet && '<TabletLayout />'}
{isDesktop && '<DesktopLayout />'}
</p>
);
}
export function UseMediaQueryPreferencesExample() {
const prefersReducedMotion = useMediaQuery(
'(prefers-reduced-motion: reduce)',
);
return (
<p className={prefersReducedMotion ? 'no-animations' : 'animated-border'}>
Content
</p>
);
}
export function UseMediaQueryOrientationExample() {
const isPortrait = useMediaQuery('(orientation: portrait)');
return <p>{isPortrait ? 'Portrait mode' : 'Landscape mode'}</p>;
}
@@ -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;
}
@@ -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';
<PageTitle
title="useMediaQuery"
description="Hook to evaluate and react to CSS media queries programmatically."
/>
<Banner
variant="warning"
text="This hook is currently intended for internal use within the design system."
/>
## Usage
The `useMediaQuery` hook allows you to evaluate CSS media queries in your components, enabling responsive design and behavior.
<Snippet
align="center"
py={4}
height={150}
open={true}
preview={<UseMediaQueryThemeExample />}
code={useMediaQueryUsageSnippet}
/>
## Parameters
<PropsTable data={useMediaQueryParamDefs} isHook />
## Return Value
<PropsTable data={useMediaQueryReturnDefs} isHook />
## Examples
### Responsive Layouts
Adapt layout based on different screen sizes.
<Snippet
align="center"
py={4}
height={200}
open={true}
preview={<UseMediaQueryResponsiveExample />}
code={useMediaQueryResponsiveSnippet}
/>
### User Preferences
Respect user system preferences.
<Snippet
align="center"
py={4}
height={150}
open={true}
preview={<UseMediaQueryPreferencesExample />}
code={useMediaQueryPreferencesSnippet}
/>
### Device Orientation
Detect screen orientation.
<Snippet
align="center"
py={4}
height={150}
open={true}
preview={<UseMediaQueryOrientationExample />}
code={useMediaQueryOrientationSnippet}
/>
<ChangelogComponent component="use-media-query" />
@@ -1,36 +0,0 @@
import { type PropDef } from '@/utils/propDefs';
export const useMediaQueryParamDefs: Record<string, PropDef> = {
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<string, PropDef> = {
matches: {
type: 'boolean',
description: 'True if the media query currently matches',
},
};
@@ -1,54 +0,0 @@
export const useMediaQueryUsageSnippet = `import { useMediaQuery } from '@backstage/ui';
function MyComponent() {
const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
return (
<p>
{isDarkMode
? 'User prefers Dark mode'
: 'User prefers Light mode'
}
</p>
);
}`;
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 (
<p>
{isMobile && <MobileLayout />}
{isTablet && <TabletLayout />}
{isDesktop && <DesktopLayout />}
</p>
);
}`;
export const useMediaQueryPreferencesSnippet = `import { useMediaQuery } from '@backstage/ui';
function AccessibleComponent() {
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
return (
<p className={prefersReducedMotion ? 'no-animations' : 'animated-border'}>
Content
</p>
);
}`;
export const useMediaQueryOrientationSnippet = `import { useMediaQuery } from '@backstage/ui';
function OrientationAware() {
const isPortrait = useMediaQuery('(orientation: portrait)');
return (
<p>
{isPortrait ? 'Portrait mode' : 'Landscape mode'}
</p>
);
}`;
-62
View File
@@ -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';
<PageTitle
title="useStyles"
description="React hook to get class names and data attributes for a component with responsive support"
/>
<Banner
variant="warning"
text="This hook is currently intended for internal use within the design system."
/>
## 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)
<CodeBlock code={useStylesUsageSnippet} />
## Parameters
<PropsTable data={useStylesPropsDefs} isHook />
## Return Value
<PropsTable data={useStylesReturnDefs} isHook />
## How It Works
### Component Definition
A component definition describes how a component should be styled:
<CodeBlock code={useStylesDefsSnippet} />
<Banner
variant="info"
text="The data-attributes definition should be kebab-case instead of camelCase."
/>
### Data Attributes
Data attributes enable state-based styling through CSS selectors. The hook automatically adds `data-*` to the attributes based on the defined props:
<CodeBlock code={useStylesAttributesSnippet} />
<ChangelogComponent component="use-styles" />
@@ -1,58 +0,0 @@
import { type PropDef } from '@/utils/propDefs';
export const useStylesPropsDefs: Record<string, PropDef> = {
componentDefinition: {
type: 'complex',
complexType: {
name: 'ComponentDefinition',
properties: {
classNames: {
type: 'Record<string, string>',
required: true,
},
dataAttributes: {
type: 'Record<string, readonly (string | number | boolean)[]>',
required: false,
},
utilityProps: {
type: 'string[]',
required: false,
},
},
},
},
props: {
type: 'enum',
description: 'All component props',
values: ['{ [key: string]: any; }'],
},
};
export const useStylesReturnDefs: Record<string, PropDef> = {
classNames: {
type: 'enum',
values: ['Record<string, string>'],
description:
"The component's class names mapped to their style definitions",
},
dataAttributes: {
type: 'enum',
values: ['Record<string, string>'],
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<string, any>'],
description: 'The original props with utility props removed',
},
};
@@ -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<ButtonProps>) {
const { classNames, dataAttributes, utilityClasses, cleanedProps } =
useStyles(buttonDefinition, {
variant,
size,
'is-disabled': isDisabled,
...props,
});
return (
<button
className={\`\${classNames.root} \${utilityClasses}\`}
{...dataAttributes}
{...cleanedProps}
>
{props.icon && <span className={classNames.icon}>{props.icon}</span>}
{props.children}
</button>
);
}`;
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;
}`;