Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions code/core/src/manager/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { addons } from 'storybook/manager-api';
import { Global, createGlobal } from 'storybook/theming';

import { Layout } from './components/layout/Layout';
import { useLayout } from './components/layout/LayoutProvider';
import Panel from './container/Panel';
import Preview from './container/Preview';
import Sidebar from './container/Sidebar';
Expand All @@ -21,8 +20,6 @@ type Props = {
};

export const App = ({ managerLayoutState, setManagerLayoutState, pages, hasTab }: Props) => {
const { setMobileAboutOpen } = useLayout();

/**
* Lets us tell the UI whether or not keyboard shortcuts are enabled, in places where it's not
* convenient to load the addons singleton to figure it out.
Expand Down Expand Up @@ -67,7 +64,7 @@ export const App = ({ managerLayoutState, setManagerLayoutState, pages, hasTab }
managerLayoutState={managerLayoutState}
setManagerLayoutState={setManagerLayoutState}
slotMain={<Preview id="main" withLoader />}
slotSidebar={<Sidebar onMenuClick={() => setMobileAboutOpen((state) => !state)} />}
slotSidebar={<Sidebar />}
slotPanel={<Panel />}
slotPages={pages.map(({ id, render: Content }) => (
<Content key={id} />
Expand Down
188 changes: 188 additions & 0 deletions code/core/src/manager/components/a11y/A11yStatement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { FC } from 'react';
import React from 'react';

import { H2, H3, H4, H5, Link } from 'storybook/internal/components';

import { shortcutToHumanString } from 'storybook/manager-api';
import { styled } from 'storybook/theming';

const Container = styled.div(({ theme }) => ({
fontSize: theme.typography.size.s2,
}));

const Kbd = styled.kbd(({ theme }) => ({
background: theme.base === 'light' ? 'rgba(0,0,0,0.05)' : 'rgba(255,255,255,0.05)',
borderRadius: theme.appBorderRadius,
border: `1px solid ${theme.appBorderColor}`,
display: 'inline-block',
padding: '2px 6px',
fontFamily: theme.typography.fonts.mono,
}));

const Shortcut: FC<{ shortcut: string }> = ({ shortcut }) => {
const human = shortcutToHumanString([shortcut]);
return <Kbd aria-label={shortcut}>{human}</Kbd>;
};
const Code = styled.pre(({ theme }) => ({
background: theme.base === 'light' ? 'rgba(0, 0, 0, 0.05)' : theme.appBorderColor,
fontSize: theme.typography.size.s2 - 1,
margin: '4px 0 16px',
}));

const A11yStatement: FC = () => (
<Container>
<H2>Accessibility Statement for Storybook</H2>
<p>This is an accessibility statement for the Storybook application.</p>

<H3>Conformance status</H3>
<p>
The{' '}
<Link href="https://www.w3.org/WAI/standards-guidelines/wcag/">
Web Content Accessibility Guidelines (WCAG)
</Link>{' '}
defines requirements for designers and developers to improve accessibility for people with
disabilities. It defines three levels of conformance: Level A, Level AA, and Level AAA.
</p>
<p>
Storybook is <strong>partially conformant</strong> with <strong>WCAG 2.2 level AA</strong>.
Partially conformant means that some parts of the content do not fully conform to the
accessibility standard.
</p>

<H3>Measures to support accessibility</H3>
<p>
The Storybook maintainers take the following measures to ensure the accessibility of
Storybook:
</p>
<ul>
<li>Include accessibility throughout our development workflow</li>
<li>Test new features for accessibility issues</li>
<li>Monitor Storybook automatically for accessibility regressions</li>
<li>Request review from external accessibility experts on specific features</li>
</ul>

<H3>Feedback</H3>
<p>
We welcome your feedback on the accessibility of Storybook. Please let us know if you
encounter accessibility barriers on Storybook by{' '}
<Link href="https://github.com/storybookjs/storybook/issues/new?template=bug_report.yml">
filling a bug report
</Link>
.
</p>
<p>We try to respond to feedback within one to two weeks.</p>

<H3>Compatibility</H3>
<p>Storybook is designed to be compatible with the following assistive technologies:</p>
<ul>
<li>Chrome with NVDA on Windows</li>
<li>Firefox with NVDA on Windows</li>
<li>Safari with VoiceOver on Mac</li>
{/* <li>Firefox with Orca on ArchLinux</li> TODO */}
</ul>
<p>
Storybook is not tested with the following technologies and may not be compatible with them:
</p>
<ul>
<li>Browsers older than Chrome 131, Edge 134, Firefox 136, Safari 18.3, and Opera 117</li>
<li>The Narrator and JAWS screen readers</li>
</ul>

<H3>Technical specifications</H3>
<p>
Accessibility of Storybook relies on the following technologies to work with the particular
combination of web browser and any assistive technologies or plugins installed on your
computer:
</p>
<ul>
<li>HTML</li>
<li>WAI-ARIA</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>
<p>These technologies are relied upon for conformance with the accessibility standards used.</p>

<H3>Limitations and alternatives</H3>
<p>
Despite our best efforts to ensure the accessibility of Storybook, there are some limitations.
Below is a description of the most impactful limitations, and potential solutions.
</p>
<p>
Please{' '}
<Link href="https://github.com/storybookjs/storybook/issues/new?template=bug_report.yml">
fill a bug report
</Link>{' '}
if you observe an issue not listed below or on our{' '}
<Link href="https://github.com/orgs/storybookjs/projects/23/views/1">
accessibility bug tracker
</Link>
.
</p>

<H4>Sidebar navigation is not accessible with the keyboard</H4>
<p>
Navigating through stories in the sidebar is difficult, and the context menus next to each
component (which let you run tests for that specific component) are not reachable by keyboard.
We are currently testing an alternative design of the sidebar that works well with keyboards
and major screen readers.
</p>
<p>
If you use the search bar to find specific stories, press the <Shortcut shortcut="ArrowUp" />{' '}
and <Shortcut shortcut="ArrowDown" /> keys from the search input to navigate to search
results.
</p>
<p>
If you use the sidebar directly, press <Shortcut shortcut="Tab" /> to navigate between content
sections, and press <Shortcut shortcut="ArrowUp" /> and <Shortcut shortcut="ArrowDown" /> to
enter a content section when on the title of a section. Each section is immediately followed
by a button to expand all items within the section. Make sure to expand all items before
navigating in the section, as expanding and collapsing individual items is not currently
possible with the keyboard alone.
</p>

<H4>Storybook Vitest addon limitations</H4>
<H5>Test result are not announced</H5>
<p>
The test widget provided by the{' '}
<Link href="https://storybook.js.org/docs/writing-tests/integrations/vitest-addon">
Vitest addon
</Link>{' '}
at the bottom of the sidebar allows you to run tests on all stories, but it does not report
when tests are done running. We will implement live announcements in a future update to
address this issue. If you have few tests to run, results will be available after
approximately ten seconds. If you have hundreds of tests, it can take a few minutes for tests
to complete. Please wait and recheck the test widget manually for results.
</p>
<H5>Tests cannot be run per component</H5>
<p>
It is not currently possible to run tests on a specific component with the keyboard only. We
will fix this issue alongside the sidebar navigation improvements. In the meantime, you can
run tests in your terminal application via Vitest, which can{' '}
<Link href="https://vitest.dev/guide/filtering">run tests for specific files</Link>:
</p>
<Code>vitest --project=storybook</Code>

<H4>Interactive stories always auto-play</H4>
<p>
Interactive stories on Storybook play automatically as you navigate to them. This is harmful
to some users, but disabling that feature requires a few underlying changes to how Storybook
functions.. We are working on a solution to disable autoplay based on user motion preferences.
In the meantime, please ask your Storybook administrator to follow the{' '}
<Link href="https://github.blog/engineering/user-experience/how-to-make-storybook-interactions-respect-user-motion-preferences/">
workaround
</Link>{' '}
kindly provided by the GitHub Design System team.
</p>

<H3>Assessment approach</H3>
<p>Storybook assessed the accessibility of Storybook by the following approaches:</p>
<ul>
<li>Self-evaluation</li>
</ul>

<H3>Date</H3>
<p>This statement was created on 2 January 2026.</p>
</Container>
);

export { A11yStatement };
10 changes: 10 additions & 0 deletions code/core/src/manager/components/layout/LayoutProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { useMediaQuery } from '../../hooks/useMedia';
type LayoutContextType = {
isMobileMenuOpen: boolean;
setMobileMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
isMobileA11yStatementOpen: boolean;
setMobileA11yStatementOpen: React.Dispatch<React.SetStateAction<boolean>>;
isMobileAboutOpen: boolean;
setMobileAboutOpen: React.Dispatch<React.SetStateAction<boolean>>;
isMobilePanelOpen: boolean;
Expand All @@ -18,6 +20,8 @@ type LayoutContextType = {
const LayoutContext = createContext<LayoutContextType>({
isMobileMenuOpen: false,
setMobileMenuOpen: () => {},
isMobileA11yStatementOpen: false,
setMobileA11yStatementOpen: () => {},
isMobileAboutOpen: false,
setMobileAboutOpen: () => {},
isMobilePanelOpen: false,
Expand All @@ -28,10 +32,12 @@ const LayoutContext = createContext<LayoutContextType>({

export const LayoutProvider: FC<
PropsWithChildren & {
/* Helps with testing components that depend on LayoutProvider */
forceDesktop?: boolean;
}
> = ({ children, forceDesktop }) => {
const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isMobileA11yStatementOpen, setMobileA11yStatementOpen] = useState(false);
const [isMobileAboutOpen, setMobileAboutOpen] = useState(false);
const [isMobilePanelOpen, setMobilePanelOpen] = useState(false);
const isDesktop = forceDesktop ?? useMediaQuery(`(min-width: ${BREAKPOINT}px)`);
Expand All @@ -41,6 +47,8 @@ export const LayoutProvider: FC<
() => ({
isMobileMenuOpen,
setMobileMenuOpen,
isMobileA11yStatementOpen,
setMobileA11yStatementOpen,
isMobileAboutOpen,
setMobileAboutOpen,
isMobilePanelOpen,
Expand All @@ -51,6 +59,8 @@ export const LayoutProvider: FC<
[
isMobileMenuOpen,
setMobileMenuOpen,
isMobileA11yStatementOpen,
setMobileA11yStatementOpen,
isMobileAboutOpen,
setMobileAboutOpen,
isMobilePanelOpen,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useEffect } from 'react';

import type { Meta, StoryObj } from '@storybook/react-vite';

import { ManagerContext } from 'storybook/manager-api';
import { within } from 'storybook/test';

import { LayoutProvider, useLayout } from '../../layout/LayoutProvider';
import { MobileA11yStatement } from './MobileA11yStatement';

/** A helper component to open the about page via the MobileLayoutContext */
const OpenAboutHelper = ({ children }: { children: any }) => {
const { setMobileA11yStatementOpen } = useLayout();
useEffect(() => {
setMobileA11yStatementOpen(true);
}, [setMobileA11yStatementOpen]);
return children;
};
Comment on lines +11 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the comment — it mentions "about page" but opens the accessibility statement.

The comment appears to be copy-pasted from MobileAbout.stories.tsx and wasn't updated for this context.

🔎 Proposed fix
-/** A helper component to open the about page via the MobileLayoutContext */
+/** A helper component to open the accessibility statement page via the LayoutContext */
 const OpenAboutHelper = ({ children }: { children: any }) => {

Also consider renaming the component to OpenA11yStatementHelper for clarity.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** A helper component to open the about page via the MobileLayoutContext */
const OpenAboutHelper = ({ children }: { children: any }) => {
const { setMobileA11yStatementOpen } = useLayout();
useEffect(() => {
setMobileA11yStatementOpen(true);
}, [setMobileA11yStatementOpen]);
return children;
};
/** A helper component to open the accessibility statement page via the LayoutContext */
const OpenAboutHelper = ({ children }: { children: any }) => {
const { setMobileA11yStatementOpen } = useLayout();
useEffect(() => {
setMobileA11yStatementOpen(true);
}, [setMobileA11yStatementOpen]);
return children;
};
🤖 Prompt for AI Agents
In code/core/src/manager/components/mobile/a11y/MobileA11yStatement.stories.tsx
around lines 11-18, the top comment incorrectly says "about page" though the
helper opens the accessibility statement; update the comment to accurately
describe that the component opens the mobile accessibility statement (e.g., "A
helper component to open the mobile accessibility statement via the
MobileLayoutContext"), and optionally rename the component from OpenAboutHelper
to OpenA11yStatementHelper (and update all references/usages and the export
name) to reflect its purpose.


const meta = {
component: MobileA11yStatement,
title: 'Mobile/AccessibilityStatement',
globals: { sb_theme: 'light' },
parameters: {
layout: 'fullscreen',
viewport: {
defaultViewport: 'mobile1',
},
chromatic: { viewports: [320] },
},
decorators: [
(storyFn) => {
return (
<ManagerContext.Provider
value={
{
api: {
getCurrentVersion: () => ({
version: '7.2.0',
}),
},
} as any
}
>
<LayoutProvider>
<OpenAboutHelper>{storyFn()}</OpenAboutHelper>
</LayoutProvider>
</ManagerContext.Provider>
);
},
],
} satisfies Meta<typeof MobileA11yStatement>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {};
export const Dark: Story = {
globals: { sb_theme: 'dark' },
};

export const Closed: Story = {
play: async ({ canvasElement }) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const closeButton = within(canvasElement).getByText('Back');
closeButton.click();
},
};
Loading
Loading