Skip to main content

Writing stories

Where stories live

Stories are colocated with the components they document, anywhere below src/<platform>/:

src/shop/client/components/atoms/facet/
├── Facet.tsx
└── Facet.stories.tsx

There is no stories glob to maintain in main.ts — discovery is automatic (see below). Two file patterns are picked up:

  • *.stories.js|jsx|mjs|ts|tsx — component stories
  • *.mdx — documentation pages

How stories are discovered

Instead of a static glob, defineConfig() registers a loader that resolves stories the same way the shadowing build resolves source files, for the tenant and platform selected when you started archibald storybook:

  1. The loader recursively scans src/<selected platform>/ and — when the selected platform is not the default one — also src/<default platform>/ (the fallback tree).
  2. Every file matching the story patterns above is collected, including files inside tenant/<tenant>/ override folders.
  3. For matches from the default-platform fallback tree, the shadowing resolver is consulted: if the story is shadowed by a more specific file for the selected platform/tenant chain, the base story is dropped and only the override (found in step 1) is served.

The result: running Storybook for platform portal shows portal's own stories plus the default platform's stories that portal does not override — exactly the set of components that platform actually ships.

Inside stories, always import components through the base path; the babel shadowing loader rewrites the import to the most specific override at compile time:

import * as Facet from 'shop/client/components/atoms/facet/Facet';

To override a story for a specific platform, place a story file at the matching path in that platform's tree (e.g. src/portal/client/components/atoms/facet/Facet.stories.tsx) — it replaces the base story when that platform is selected.

A real example

Trimmed from the shop template's Facet.stories.tsx:

src/shop/client/components/atoms/facet/Facet.stories.tsx (excerpt)
import { type Facet as FacetType } from '@archibald/search';
import type { StoryObj } from '@storybook/react';

import { fn } from 'storybook/test';

import * as Facet from 'shop/client/components/atoms/facet/Facet';

const sampleFacet: FacetType = {
name: 'Color',
visible: true,
type: 'multiSelect',
id: 'f-color',
key: 'color',
priority: 1,
values: [
{ name: 'Red', count: 12, selected: false },
{ name: 'Blue', count: 8, selected: true }
]
};

export default {
title: 'Atoms/Facet',
component: Facet.Root,
parameters: { layout: 'padded' }
};

export const Default: StoryObj = {
args: { numberOfShownFacetValues: 4, facet: sampleFacet, onApply: fn() },
render: ({ facet, numberOfShownFacetValues, onApply }) => (
<Facet.Root facet={facet} numberOfShownFacetValues={numberOfShownFacetValues} onApply={onApply} autoApply>
<Facet.List>
<Facet.MultiSelect />
</Facet.List>
<Facet.ShowMore />
</Facet.Root>
)
};

This is standard Storybook CSF — args, argTypes, render, and autodocs all work as documented upstream. The Archibald-specific part is only the base-path import.

App context with StorybookProvider

Components that use framework hooks (translations, data client, app client, router) need the same contexts they get in the application. StorybookProvider from @archibald/storybook provides them — it wraps its children in the TestingProviders used by unit tests plus an in-memory router, so Storybook and Jest share one context mechanism (see StorybookProvider in the testing docs).

The template applies it globally as a decorator in .storybook/preview.tsx, seeded with translation data:

.storybook/context/i18n.ts
import { DataClient, AppClient, MESSAGES_KEY } from '@archibald/core';

import { en } from '../i18n/en';

export function createI18nContext() {
const dataClient = new DataClient();
dataClient.set([MESSAGES_KEY, 'en'], {
hasBeenLoaded: true,
status: 'done',
response: en
});

const appClient = new AppClient();
appClient.language = 'en';

return { dataClient, appClient };
}

The context prop accepts the same shape as createContextdataClient, appClient, history, translate, and friends — so a single story can also opt into its own context by wrapping its render output in another StorybookProvider with pre-seeded data (for example, CMS or API responses placed in a DataClient).

Styling

Stories run through the application's style pipeline, not a Storybook-specific one:

  • The global stylesheet is imported once in preview.tsx (import 'shop/resources/scss/base/global.scss'), giving every story the base theme. The provider also adds the theme class to document.body while a story is mounted.
  • SCSS imports resolve through the shadowing importer, so tenant/platform style overrides apply to stories exactly as in the app.
  • CSS modules behave according to your cli/style/modules configuration.
  • With cli/optimization/svgComponents enabled, SVGs import as React components via SVGR, as in the app build.