Skip to content

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-vite framework
  • Config: .storybook/
  • Stories: co-located as src/components/**/*.stories.tsx
Terminal window
npm run storybook

Starts the dev server on http://localhost:6006 with hot reload.

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

Stories live next to the component they cover and follow the project’s normal conventions — # path aliases, and full type safety.

src/components/react/RoleBadge.stories.tsx
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' } }
  1. Import through the # alias. Relative imports are not allowed, in stories as anywhere else.

  2. Type both ends. Use satisfies Meta<typeof Component> on the default export and StoryObj<typeof meta> for each story. Stories live under src/, so npm run type-check verifies that every story’s args match the component’s props.

  3. 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 ContactForm and view/ContactFormView.

@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.

.storybook/preview.ts loads the same three stylesheets as src/layouts/Base.astro, so components render identically in both places:

.storybook/preview.ts
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.

@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.

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.