Backstage UI: Add VisuallyHidden component.

Adds a new VisuallyHidden component for hiding content visually while keeping it accessible to screen readers.

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2025-10-21 10:24:13 +02:00
parent 8a2725b100
commit 1ef3ca48d6
13 changed files with 322 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Added new VisuallyHidden component for hiding content visually while keeping it accessible to screen readers.
@@ -0,0 +1,51 @@
import { PropsTable } from '@/components/PropsTable';
import { Snippet } from '@/components/Snippet';
import { CodeBlock } from '@/components/CodeBlock';
import { VisuallyHiddenSnippet } from '@/snippets/stories-snippets';
import {
visuallyHiddenPropDefs,
visuallyHiddenUsageSnippet,
visuallyHiddenDefaultSnippet,
visuallyHiddenExampleUsageSnippet,
} from './visually-hidden.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="VisuallyHidden"
description="Visually hides content while keeping it accessible to screen readers."
/>
<Snippet
align="start"
py={4}
preview={<VisuallyHiddenSnippet story="Default" />}
code={visuallyHiddenDefaultSnippet}
/>
## Usage
<CodeBlock code={visuallyHiddenUsageSnippet} />
## API reference
<PropsTable data={visuallyHiddenPropDefs} />
## Examples
### Example Usage
Here's an example of providing screen reader context for a list of links in a footer.
<Snippet
align="start"
py={4}
preview={<VisuallyHiddenSnippet story="ExampleUsage" />}
code={visuallyHiddenExampleUsageSnippet}
open
/>
<Theming component="VisuallyHidden" />
<ChangelogComponent component="visually-hidden" />
@@ -0,0 +1,47 @@
import {
classNamePropDefs,
stylePropDefs,
type PropDef,
} from '@/utils/propDefs';
export const visuallyHiddenPropDefs: Record<string, PropDef> = {
children: {
type: 'enum',
values: ['ReactNode'],
responsive: false,
},
...classNamePropDefs,
...stylePropDefs,
};
export const visuallyHiddenUsageSnippet = `import { VisuallyHidden } from '@backstage/ui';
<VisuallyHidden>
This content is visually hidden but accessible to screen readers
</VisuallyHidden>`;
export const visuallyHiddenDefaultSnippet = `<Flex direction="column" gap="4">
<Text as="p">
This text is followed by a paragraph that is visually hidden but
accessible to screen readers. Try using a screen reader to hear it, or
inspect the DOM to see it's there.
</Text>
<VisuallyHidden>
This content is visually hidden but accessible to screen readers
</VisuallyHidden>
</Flex>`;
export const visuallyHiddenExampleUsageSnippet = `<Flex direction="column" gap="4">
<VisuallyHidden>
<Text as="h2">Footer links</Text>
</VisuallyHidden>
<Text as="p">
<a href="#">About us</a>
</Text>
<Text as="p">
<a href="#">Jobs</a>
</Text>
<Text as="p">
<a href="#">Terms and Conditions</a>
</Text>
</Flex>`;
@@ -29,6 +29,7 @@ import * as HeaderPageStories from '../../../packages/ui/src/components/HeaderPa
import * as TableStories from '../../../packages/ui/src/components/Table/Table.stories';
import * as TagGroupStories from '../../../packages/ui/src/components/TagGroup/TagGroup.stories';
import * as PasswordFieldStories from '../../../packages/ui/src/components/PasswordField/PasswordField.stories';
import * as VisuallyHiddenStories from '../../../packages/ui/src/components/visuallyHidden/VisuallyHidden.stories';
// Helper function to create snippet components
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -74,3 +75,6 @@ export const HeaderSnippet = createSnippetComponent(HeaderStories);
export const HeaderPageSnippet = createSnippetComponent(HeaderPageStories);
export const TableSnippet = createSnippetComponent(TableStories);
export const TagGroupSnippet = createSnippetComponent(TagGroupStories);
export const VisuallyHiddenSnippet = createSnippetComponent(
VisuallyHiddenStories,
);
+5
View File
@@ -182,6 +182,11 @@ export const components: Page[] = [
slug: 'tooltip',
status: 'alpha',
},
{
title: 'VisuallyHidden',
slug: 'visually-hidden',
status: 'alpha',
},
];
export type ScreenSize = {
+14
View File
@@ -743,6 +743,11 @@ export const componentDefinitions: {
readonly arrow: 'bui-TooltipArrow';
};
};
readonly VisuallyHidden: {
readonly classNames: {
readonly root: 'bui-VisuallyHidden';
};
};
};
// @public (undocumented)
@@ -1534,4 +1539,13 @@ export interface UtilityProps extends SpaceProps {
// (undocumented)
rowSpan?: Responsive<Columns | 'full'>;
}
// @public
export const VisuallyHidden: (props: VisuallyHiddenProps) => JSX_2.Element;
// @public
export interface VisuallyHiddenProps extends ComponentProps<'div'> {
// (undocumented)
children?: React.ReactNode;
}
```
@@ -0,0 +1,32 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@layer tokens, base, components, utilities;
@layer components {
.bui-VisuallyHidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(100%);
white-space: nowrap;
border: 0;
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Meta, StoryObj } from '@storybook/react-vite';
import { VisuallyHidden } from './VisuallyHidden';
import { Text } from '../Text';
import { Flex } from '../Flex';
const meta = {
title: 'Backstage UI/VisuallyHidden',
component: VisuallyHidden,
parameters: {
docs: {
description: {
component:
'Visually hides content while keeping it accessible to screen readers. Commonly used for descriptive labels, and other screen-reader-only content.',
},
},
},
} satisfies Meta<typeof VisuallyHidden>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<Flex direction="column" gap="4">
<Text as="p">
This text is followed by a paragraph that is visually hidden but
accessible to screen readers. Try using a screen reader to hear it, or
inspect the DOM to see it's there.
</Text>
<VisuallyHidden>
This content is visually hidden but accessible to screen readers
</VisuallyHidden>
</Flex>
),
};
export const ExampleUsage: Story = {
render: () => (
<Flex direction="column" gap="4">
<VisuallyHidden>
<Text as="h2">Footer links</Text>
</VisuallyHidden>
<Text as="p">
<a href="#">About us</a>
</Text>
<Text as="p">
<a href="#">Jobs</a>
</Text>
<Text as="p">
<a href="#">Terms and Conditions</a>
</Text>
<Text as="p" variant="body-small" color="secondary">
(Screen readers hear: "Footer links" followed by the list of links)
</Text>
</Flex>
),
};
@@ -0,0 +1,41 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useStyles } from '../../hooks/useStyles';
import { VisuallyHiddenProps } from './types';
import styles from './VisuallyHidden.module.css';
import clsx from 'clsx';
/**
* Visually hides content while keeping it accessible to screen readers.
* Useful for descriptive labels and other screen-reader-only content.
*
* Note: This component is for content that should ALWAYS remain visually hidden.
* For skip links that become visible on focus, use a different approach.
*
* @public
*/
export const VisuallyHidden = (props: VisuallyHiddenProps) => {
const { classNames, cleanedProps } = useStyles('VisuallyHidden', props);
const { className, ...rest } = cleanedProps;
return (
<div
className={clsx(classNames.root, styles[classNames.root], className)}
{...rest}
/>
);
};
@@ -0,0 +1,18 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { VisuallyHidden } from './VisuallyHidden';
export type { VisuallyHiddenProps } from './types';
@@ -0,0 +1,26 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentProps } from 'react';
/**
* Properties for {@link VisuallyHidden}
*
* @public
*/
export interface VisuallyHiddenProps extends ComponentProps<'div'> {
children?: React.ReactNode;
}
+1
View File
@@ -52,6 +52,7 @@ export * from './components/Link';
export * from './components/Select';
export * from './components/Skeleton';
export * from './components/Switch';
export * from './components/VisuallyHidden';
// Types
export * from './types';
@@ -395,4 +395,9 @@ export const componentDefinitions = {
arrow: 'bui-TooltipArrow',
},
},
VisuallyHidden: {
classNames: {
root: 'bui-VisuallyHidden',
},
},
} as const satisfies Record<string, ComponentDefinition>;