Storybook
Storybook renders the project’s React components in isolation, with live controls, generated prop tables and an automated accessibility audit.
- Version: Storybook 10.6 with the
@storybook/react-viteframework - Config:
.storybook/ - Stories: co-located as
src/components/**/*.stories.tsx
Running Storybook
Section titled “Running Storybook”npm run storybookStarts the dev server on http://localhost:6006 with hot reload.
npm run build-storybookProduces a static build in storybook-static/, which is git-ignored.
What is covered
Section titled “What is covered”| Component type | Location | In Storybook |
|---|---|---|
| React (client) | src/components/react |
Yes |
| Astro (server-rendered) | src/components/astro |
No |
| Dashboard (protected) | src/components/dashboard |
No — all .astro today |
Writing a story
Section titled “Writing a story”Stories live next to the component they cover and follow the project’s normal
conventions — # path aliases, and full type safety.
import type { Meta, StoryObj } from '@storybook/react-vite'
import RoleBadge from '#components/react/RoleBadge'
const meta = { title: 'React/RoleBadge', component: RoleBadge, args: { role: 'member' },} satisfies Meta<typeof RoleBadge>
export default meta
type Story = StoryObj<typeof meta>
export const Admin: Story = { args: { role: 'admin' } }-
Import through the
#alias. Relative imports are not allowed, in stories as anywhere else. -
Type both ends. Use
satisfies Meta<typeof Component>on the default export andStoryObj<typeof meta>for each story. Stories live undersrc/, sonpm run type-checkverifies that every story’sargsmatch the component’s props. -
Keep stories prop-driven. A component that reads Clerk session state or the database should take that data as props, or have a presentational sibling to story instead — see
ContactFormandview/ContactFormView.
Components that read the URL
Section titled “Components that read the URL”@fpkit/react’s Breadcrumb derives its trail from window.location.pathname
and treats its routes prop only as a lookup table for segment names. Inside
Storybook the pathname is always /iframe.html, so
astro-breadcrumb.stories.tsx wraps its stories in an atPath() decorator that
patches the URL during the first render and restores it on unmount:
const atPath = (pathname: string): Decorator => function AtPathDecorator(Story) { const [originalUrl] = useState(() => { const previous = `${window.location.pathname}${window.location.search}` window.history.pushState(null, '', `${pathname}${window.location.search}`) return previous })
useEffect( () => () => { window.history.replaceState(null, '', originalUrl) }, [originalUrl] )
return <Story /> }Reuse this pattern for any component that reads from window.location.
Styling
Section titled “Styling”.storybook/preview.ts loads the same three stylesheets as
src/layouts/Base.astro, so components render identically in both places:
import '@fpkit/acss/styles'import '../src/styles/index.scss'import '../src/styles/salt/index.css'The SCSS source is imported rather than the compiled src/styles/index.css,
so Storybook never depends on npm run sass:build having run first.
Accessibility
Section titled “Accessibility”@storybook/addon-a11y runs axe against every story and reports findings in the
Accessibility panel. It is configured with a11y: { test: 'todo' }, so
violations are surfaced without failing the story. Treat them as review items
alongside the wcag-compliance-reviewer skill.
Configuration notes
Section titled “Configuration notes”That standalone config also restates the project’s #* path aliases, which
Storybook cannot read from astro.config.mjs. Each top-level src/ directory is
aliased individually rather than with a catch-all #* pattern, so #-prefixed
subpath imports inside node_modules packages keep resolving against their own
package.json.
Story globs are scoped to src/components on purpose. A broader src/**/*.mdx
glob sweeps in the Starlight content collection under src/content, which
Storybook cannot index.