-
Notifications
You must be signed in to change notification settings - Fork 0
Configuration
🔄 Auto-generated mirror. Canonical source:
docs/configuration.md. Edit there — changes here are overwritten by CI.
Nav: Quick start · Writing cases · Hierarchy · Tweaks · Theming · Style engines · Documentation panel · Writing placard docs · Testing · CLI · AI agents · Configuration
Display Case is configured by a single display-case.config.ts (or .tsx) file at the root of the package it showcases. The file must default-export defineConfig(...); if it does not, the CLI errors. defineConfig is an identity helper that exists purely to give the config file full type-checking and inference.
// display-case.config.ts
import { defineConfig } from '@awarebydefault/display-case'
import { ThemeProvider } from './src/components/theme-provider'
export default defineConfig({
title: 'Display Case',
roots: ['src/components/**/*.case.tsx'],
globalStyles: ['./src/tokens.css', './src/components.css'],
decorator: ThemeProvider,
baselineDir: 'baselines',
})The CLI looks for display-case.config.ts then display-case.config.tsx in the package directory.
| Key | Type | Required | Default | Description |
|---|---|---|---|---|
title |
string |
yes | — | Shown in the browsing chrome and the manifest. |
roots |
string[] |
yes | — | Globs (relative to the package) locating *.case.tsx files. |
primer |
string |
no | none | Path (relative to the package) to an .mdx document rendered as the Primer reading page. When set, a Primer tab joins the mode switch. See primer. |
landing |
'primer' | 'components' | 'exhibits' |
no | first present mode | Which browse mode the chrome lands on at /. Honored only when that mode is present; otherwise the first present mode (primer → components → exhibits). See landing. |
nav |
NavConfig |
no | none | Information-architecture configuration for the Exhibits mode: folder-derivation toggle, surface→group mapping, and group order/labels/default-collapsed. See nav. |
globalStyles |
string[] |
no | none | CSS entrypoints (relative to the package) injected into previews. |
decorator |
ComponentType<{ children, level?, sourcePath?, area? }> |
no | none | Wrapper rendered around every case; also receives the active case's level, sourcePath, and area so it can wrap page/flow cases in app chrome. |
styleEngines |
StyleEngine[] |
no | none | Engines that collect render-time (CSS-in-JS) styling — emotion/MUI, styled-components — during the server render and deliver it before scripting. Pair with decorator. See styleEngines and Style engines. |
share |
string[] |
no | none | Runtime libraries to deliver once across a published showcase, beyond React (always shared). A library shared by several components is built into one cacheable vendor bundle every surface resolves to, instead of inlined into each per-component bundle. Affects only display-case publish. See share. |
baselineDir |
string |
no | .display-case/baselines |
Where visual-regression baselines are stored. |
tokens |
{ allow?: string[] } |
no | none | Design-token conformance options for --tokens. allow lists custom-property names the package may reference but does not itself define (e.g. host-app-provided tokens). See Testing. |
providers |
{ driver?, diff? } |
no | built-in | Override the visual-regression backend. When unset, the built-in Playwright/axe driver and pixelmatch/pngjs diff are loaded lazily. See providers. |
check |
{ defaultPhases?, concurrency?, structure?, graphBudget? } |
no | none | Tune the check command: which phases run by default, how many variants the a11y/visual phases scan concurrently, the structure phase's rules and severities, and the bundle-graph budgets. See check. |
a11y |
{ enabled?, themes?, exclude?, startup? } |
no | off | Live accessibility surfacing in the running browse chrome. See a11y. |
The library name. Appears in the browse shell header and in /manifest.json as the top-level title.
One or more glob patterns, resolved relative to the package directory, that select your case files. Matches under any node_modules/ are ignored, and results are de-duplicated. Most libraries need just one:
roots: ['src/components/**/*.case.tsx']CSS files concatenated and injected into both the browse shell and the isolated render document, so components render with their real tokens and styles. Paths are resolved relative to the package; a listed file that does not exist is skipped silently. See Theming.
globalStyles: ['./src/tokens.css', './src/components.css']Path (relative to the package) to an .mdx document rendered as the Primer — a long-form reading page with embedded live specimens, shown via a Primer / Cases mode switch the chrome adds to the sidebar. The MDX is bundled into its own isolated frame (like /render), so it can import any component — case files and arbitrary .tsx — without risking the browse chrome.
primer: './src/design-system/primer.mdx'Wrap each live specimen in the <Display> contract (provided to the MDX automatically — no import needed):
import { Button } from './components'
# Our design system
The wall text that orients you before browsing the cases.
<Display title="Button" subtitle="The one true action" theme="dark">
<Button variant="accent">Snapshot</Button>
</Display><Display> props:
| Prop | Type | Description |
|---|---|---|
title |
string |
Specimen title; also the sidebar table-of-contents entry and scroll anchor. |
subtitle |
string |
Optional one-line description under the title. |
theme |
'light' | 'dark' |
Optional — forces a theme scope inside the card only, so a dark-mode component reads correctly on a light page (and vice versa). Omit to inherit the page theme. |
flush |
boolean |
Optional — drop the card's own border and padding so a single self-bordered child fills the box edge-to-edge (avoids a box-within-a-box). |
appSurface |
boolean |
Optional — paint the card with the consumer design system's own canvas (--color-bg/--color-fg, the same tokens the /render frame paints) instead of the Vitrine's --dc-bg, so the specimen sits on the exact background the real app gives it. Combine with theme for the app's themed surface; degrades to --dc-bg when no --color-bg is defined. |
The sidebar reflects each <Display> title as a table-of-contents entry, grouped under the #/## heading that precedes it — each heading is itself a navigable, collapsible group header (the # page title doubles as the "top of page" entry). Any <Display>s before the first heading fall into a leading "Contents" group. Long entries truncate with an ellipsis. Scrollspy highlights the heading or section in view. Toggling the chrome's light/dark theme drives the Primer too.
The Primer is compiled by Display Case's own small MDX compiler (mdx-lite), not the full MDX toolchain. It supports a deliberately constrained dialect — exactly enough for prose interleaved with live specimens — and nothing more. Within these rules a Primer behaves like ordinary TypeScript + Markdown:
-
ES
import/exportstatements at the start of a line (column 0), single- or multi-line. They are passed through verbatim and resolved by the bundler, so a Primer imports components exactly like any.tsxmodule. - Prose in the same CommonMark + GFM flavour as placard docs — headings, bold/italic, inline code, lists, links, GFM tables, strikethrough. Raw HTML is not rendered, and fenced code is not syntax-highlighted (the same two limits).
-
Block-level specimens — a JSX element that begins a line at column 0 (a capitalized tag like
<Display …>or a<>fragment), consumed through its matching close. Standard JSX expression props work, e.g.style={{ fontSize: '1rem' }}. Author-imported components resolve through their imports;<Display>is provided automatically. - A fenced code block stays prose even when its contents look like a specimen — a
```mdxsample containing<Display>renders as code, never as a live element.
Not supported (by construction — keep specimens as their own blocks):
- Inline JSX inside a prose paragraph. Put a specimen on its own line at column 0, set off by blank lines.
- Markdown syntax inside JSX children. Text inside a specimen is literal JSX, not Markdown.
-
{expression}interpolation in prose. Expressions belong inside a specimen's JSX, not in the surrounding text.
These constraints are checked: a Primer that can't be parsed, or that has no <Display> specimen or no prose, fails the primer-present-and-used structure check rather than rendering wrong.
Which browse mode the chrome shows first when you open the root path (/): the
'primer' reading page, the 'components' kit, or the 'exhibits' surfaces. Use
'components' when the kit is the main event and the Primer is supplementary:
primer: './src/design-system/primer.mdx',
landing: 'components',The configured mode is honored only when it is present (has content);
otherwise the chrome falls back to the first present mode in the order
primer → components → exhibits (so the default, with a Primer configured, is the
Primer). This setting governs only the bare / landing — a case deep link
(/c/... or /e/...) always opens that case, and /primer always opens the
Primer.
Information architecture for the Exhibits mode — how pages and flows are grouped. All fields are optional:
nav: {
// Derive a surface's group from its case-file folder. Default: true.
deriveFromFolder: true,
// Map surfaces to groups when their folders don't mirror the IA. First match
// wins; consulted after an explicit `meta.group` and folder derivation.
surface: [
{ id: 'pricing', group: 'Marketing/Plans' }, // by component id, or a glob
{ area: 'admin', group: 'Admin' }, // or by the case's `area`
],
groups: {
order: ['Onboarding', 'Marketing', 'App'], // unlisted groups follow
labels: { app: 'Signed-in app' }, // rename a derived segment
collapsed: ['Admin'], // collapsed by default
},
// How a flow is distinguished from a page in the Exhibits sidebar:
// 'tag' (default) appends a high-vis `flow` pill after the name;
// 'glyph' prefixes the flow row with a leading glyph. Either way a flow's
// step rows are numbered and pages render plain.
flowMarker: 'tag',
}A surface's group resolves first-match-wins: explicit
meta.group → folder
(relative to the matched roots glob; route-group parens like (marketing) and
leading underscores are stripped, segments title-cased) → a surface rule → a
default group. order/labels/collapsed match a group by its segment or its
/-joined path, case-insensitively; naming a group no surface resolves to is
reported by the nav-groups-resolve structure check (a warning). See
Hierarchy → Components and Exhibits.
A single React component that wraps every rendered case (for a theme provider, context, or a fixed frame). It is applied inside React StrictMode. See Theming.
import { ThemeProvider } from './src/components/theme-provider'
// …
decorator: ThemeProvider,Besides children, the decorator receives the active case's identity so it can
wrap page and flow cases in app chrome (a nav header, sidebar, footer)
while leaving smaller components bare:
decorator?: ComponentType<{
children: ReactNode
level?: HierarchyLevel // 'atom' | … | 'page' | 'flow' (from defineCases/defineFlow)
sourcePath?: string // case file path, package-relative (folder convention)
area?: string // free-form tag from the case's meta (overrides sourcePath)
}>Per-area app chrome. A consuming app's decorator can render a page the way it looks in the real app — inside its actual navigation/layout — by branching on these:
Say a consuming app has two areas — a marketing site and the signed-in app —
each with its own header. A decorator can wrap page/flow cases in the matching
chrome and leave smaller components bare:
function Decorator({ children, level, sourcePath, area }) {
const inApp = level === 'page' || level === 'flow'
// Resolve which chrome to use: an explicit `area` tag wins; otherwise infer
// from the `page-cases/<area>/…` folder; otherwise render bare.
const which =
area ?? sourcePath?.match(/(?:^|\/)page-cases\/([^/]+)\//)?.[1]
let content = children
if (inApp && which === 'marketing') content = <SiteHeader variant="marketing">{children}</SiteHeader>
else if (inApp && which === 'app') content = <SiteHeader variant="app">{children}</SiteHeader>
return <Providers>{content}</Providers>
}Cases tagged marketing (or under page-cases/marketing/…) render inside the
marketing header; app cases render inside the signed-in header.
Display Case is unopinionated here: it supplies the signals and mandates no
vocabulary or folder layout. Tag a case via meta.area (see
Writing cases), or organize cases into area folders and read
sourcePath — whichever suits the package. Chrome that itself renders router
<Link>s (e.g. a nav bar) needs a router in context; if your nav uses a router,
provide a tiny in-memory router inside the chrome so the links resolve.
For components styled by a runtime CSS-in-JS library — emotion (and therefore
Material UI), styled-components, and peers — that emit their CSS as a side
effect of rendering. A style engine collects that styling during the server
render and delivers it in the isolated /render and Primer documents before
scripting, so those surfaces are styled without executing scripts (no flash, and
chrome-free snapshots come back styled).
styleEngines: [emotionEngine]Each engine is a factory invoked once per render for an isolated style store:
type StyleEngine = () => StyleCollector
interface StyleCollector {
wrap(node: ReactNode): ReactNode // provide the library's store (e.g. emotion CacheProvider)
collect(renderedHtml: string): string // return the <head> style markup that render used
}styleEngines (the server-side extractor) pairs with decorator
(the client/SSR provider, e.g. a MUI ThemeProvider). Omit styleEngines
entirely and the documents are byte-identical to their engine-free form. The full
recipe — emotion/MUI flagship, styled-components, and when to use globalStyles
instead — is in Style engines.
A published showcase builds each component into its own isolated
browser bundle (so no single bundler pass ever has to hold the whole catalog — the
design that keeps a large showcase buildable). React is always delivered once as a
shared, content-hashed vendor bundle that every surface references via an
importmap,
not copied into each bundle. share extends that same treatment to any other runtime
library:
share: ['markdown-to-jsx', '@emotion/react', '@emotion/styled', '@acme/design-tokens']Reach for it when several components import the same library and you don't want each per-component bundle to carry its own copy. Two reasons:
- Size — a library shared by N components is downloaded once site-wide instead of N times (each copy otherwise cached separately, never reused).
-
Correctness — a library that keeps internal state and must exist as a single
instance (a CSS-in-JS engine's style cache/context) behaves correctly only when
every surface resolves to one copy. Declaring it
shareguarantees that, the same way React is always collapsed to one copy.
Notes:
- List the package (
'markdown-to-jsx'), an explicit subpath ('@acme/icons/solid'), or a monorepo workspace package ('@acme/design-tokens') — a package defined in your repo is shared on the client just like a published one. A deep import you don't list stays inlined (still correct, just not shared). - Assumes one installed version per shared package across the showcase (true for peer dependencies by definition). Don't share a library you intentionally run at two major versions.
- Affects only
display-case publish, never the dev server. With nothing declared, React is still shared and the output is what you'd get withoutshareat all. - Not sure what to list?
display-case publishprints any library it inlined into more than one component as a candidate — add the ones worth sharing.
Where the visual-regression runner reads and writes baseline PNGs. Defaults to the gitignored cache at .display-case/baselines (local-only). Provide a path — relative to the package or absolute — to point at a committed directory and gate CI on shared baselines. See Testing.
baselineDir: 'baselines' // committed, relative to the package
// or
baselineDir: '/abs/path/to/baselines'The visual-regression backend is pluggable. providers lets you replace either half of it — the driver that opens a case render URL and captures it, the diff that compares a capture against its baseline, or both.
export default defineConfig({
title: 'Display Case',
roots: ['src/components/**/*.case.tsx'],
providers: {
driver: () => myDriver(), // optional; default = built-in Playwright + axe
diff: myDiff, // optional; default = built-in pixelmatch/pngjs
},
})When providers is omitted (or a half is left unset), Display Case falls back to its built-in default for that half. The default is lazy and optional: the Playwright/axe driver and pixelmatch/pngjs diff are imported only when a default-backed check --a11y/--visual actually runs, so browsing, snapshotting, and init never need them. See Testing. Setting a custom provider replaces the default for that half and removes the need for those packages entirely.
The reference implementations are the built-ins themselves — src/checks/providers/playwright-driver.ts and src/checks/providers/pixelmatch-diff.ts. A custom provider need only satisfy the interface.
Both providers receive the identity of the case being rendered, so an identity-aware provider can vary its behavior per case (a per-case tolerance, a name-keyed hosted service such as Percy or Chromatic, richer reporting). Pure providers simply ignore it.
interface CaseContext {
componentId: string
caseId: string
theme: 'light' | 'dark'
width: number
}type DiffFn = (
input: { baseline: Uint8Array; actual: Uint8Array },
ctx: CaseContext & { baselinePath: string },
) => DiffResult | Promise<DiffResult>
interface DiffResult {
changed: boolean
mismatch?: number // e.g. count of differing pixels, for reporting
diffImage?: Uint8Array // written next to the baseline on a change
}A custom diff that loosens tolerance for one noisy case and leaves every other case strict:
import pixelmatch from 'pixelmatch'
import { PNG } from 'pngjs'
import { defineConfig, type DiffFn } from '@awarebydefault/display-case'
const tolerantDiff: DiffFn = ({ baseline, actual }, ctx) => {
const a = PNG.sync.read(Buffer.from(baseline))
const b = PNG.sync.read(Buffer.from(actual))
if (a.width !== b.width || a.height !== b.height) return { changed: true }
// Allow a few stray pixels on the gradient case; everything else stays exact.
const allowed = ctx.caseId === 'gradient' ? 50 : 0
const diff = new PNG({ width: a.width, height: a.height })
const mismatch = pixelmatch(a.data, b.data, diff.data, a.width, a.height, {
threshold: 0.1,
})
return mismatch > allowed
? { changed: true, mismatch, diffImage: PNG.sync.write(diff) }
: { changed: false, mismatch }
}
export default defineConfig({
title: 'Display Case',
roots: ['src/components/**/*.case.tsx'],
providers: { diff: tolerantDiff },
})interface RenderDriver {
open(url: string, ctx: CaseContext): Promise<RenderedPage>
close(): Promise<void>
}
interface RenderedPage {
screenshot(): Promise<Uint8Array>
audit(): Promise<A11yViolation[]> // [] if the driver skips auditing
dispose(): Promise<void>
}
interface A11yViolation {
id: string
help: string
nodes: number
}driver is a factory: it is called once, returns a RenderDriver reused across every case, and close() runs when the check finishes. Each open() yields a RenderedPage you can screenshot() and audit(), then dispose(). A sketch:
import { defineConfig, type RenderDriver } from '@awarebydefault/display-case'
function myDriver(): RenderDriver {
const browser = /* launch your headless browser once */
return {
async open(url, ctx) {
const page = /* open `url`; ctx.theme / ctx.width are already in the URL */
return {
screenshot: () => page.capture(),
audit: async () => [], // return [] to skip a11y auditing
dispose: () => page.close(),
}
},
close: () => browser.close(),
}
}
export default defineConfig({
title: 'Display Case',
roots: ['src/components/**/*.case.tsx'],
providers: { driver: myDriver },
})Compare against the built-in createPlaywrightDriver, which launches Chromium at a fixed 1024×768 viewport with reduced motion and runs a WCAG 2 A/AA axe audit.
Tunes the check command. Two independent parts:
check: {
// Which phases run in the default (no-flag) run. Unset ⇒ included; set false to
// opt out. An opted-out phase still runs when named explicitly (e.g. --visual).
defaultPhases: { visual: false },
// How many variants the a11y/visual phases scan concurrently. Default 4.
concurrency: 4,
structure: {
// Treat every structure warning as an error for the run (same as --strict).
strict: false,
// Per-rule overrides. Each rule is on at its default severity unless set here.
rules: {
'primer-present-and-used': false, // disable a rule
'composes-lower-level': 'error', // enable/retune severity
'case-placard-coverage': { ignore: ['**/internal/**'] }, // skip paths
'atom-purity': {}, // enable an opt-in rule at default severity
'level-fit': { thresholds: { molecule: 8 } }, // rule-specific options
},
},
// Budgets for the bundle-graph (`--graph`) phase. Unset fields use the defaults.
graphBudget: { modules: 1500, perPackage: 400 },
}-
defaultPhases— aPartial<Record<'tokens' | 'a11y' | 'visual' | 'structure' | 'ssr' | 'graph', boolean>>. Drop a phase from the barecheckrun (e.g.visualwhen no baselines are committed) while keeping it available via its flag. -
concurrency— how many variants the render phases (a11y/visual) scan at once, each on its own page from a shared browser. Default4; override per run with--concurrency=N. A customproviders.drivermust tolerate concurrentopen()calls (set1if it can't). See Testing → Reporting and concurrency. -
structure.rules[id]—falsedisables the rule;'warn'/'error'enables it at that severity; an options object ({ severity?, ignore?, thresholds? }) enables it with overrides. Unset ⇒ the rule's default. The rule ids, defaults, and escape-hatch markers are listed in Testing → Structure checks. -
structure.strict— escalate all structure warnings to errors (the config equivalent ofcheck --strict). -
graphBudget— budgets for the--graphphase, which measures each component's real bundled module graph (built in isolation, so measuring can never crash the tool).modules(default1500) warns when a component's total module count exceeds it;perPackage(default400) warns when a single dependency contributes more modules than it — the barrel-import signal (e.g. importing a whole icon set). Warnings are advisory;--strictmakes them errors. The budgets are deliberately loose early-warnings — the true crash threshold is machine-dependent — so tighten them for your codebase as needed.
Surface accessibility results in the running browse chrome — a per-variant marker in the nav rail and an Accessibility panel beside the rendered case.
a11y: {
enabled: true, // default false — opt-in
themes: ['light', 'dark'], // default both; scanned + reflected per theme
exclude: ['color-contrast'], // axe rule ids to skip
startup: 'cached', // default 'off' — how the nav is populated at boot
}-
Off by default. It needs the same optional Playwright + axe toolchain as a
default-backed
check(seeproviders); when that toolchain can't launch, the panel shows an unavailable state and the server still browses normally — it never fails to start. -
On demand + cached. Only the variant you're viewing is scanned, lazily;
results are cached under
.display-case/a11y/and reused until that variant's rendered output changes (judged by a transitive-import content hash). Scans run on a background queue and never block browsing. -
startuppopulates the nav at boot (only meaningful withenabled; default'off'). It does not change the on-demand-per-viewed-variant behavior — it only decides what the nav shows before you start clicking:-
'off'— no boot-time population; a variant's marker appears only once viewed. -
'cached'— fill markers from reusable cached results, running no scans; uncached or stale variants stay unmarked until viewed. -
'refresh'— additionally scan every uncached or stale variant at boot, surfacing each verdict as it lands (reusing fresh cache without re-scanning). Work rides the same background queue, so browsing stays responsive. Nothing is scanned if the toolchain can't launch.
-
-
enabledgates only the live surface. Thedisplay-case check --a11yCI gate runs whenever invoked regardless ofenabled, but honors the sharedthemes/excludehere so the panel and the gate agree on what counts as a violation.
Display Case writes generated artifacts (the bundled output and the auto-generated render entry) to .display-case/ inside the package, and — unless overridden — keeps baselines under .display-case/baselines/. Add .display-case/ to .gitignore; it is a derived cache.
Auto-generated mirror of the Display Case docs. Edit the repo, not the wiki.
Product documentation
- AI Agents
- CLI
- Configuration
- Documentation Panel
- Examples
- Hierarchy
- Quick Start
- Style Engines
- Testing
- Theming
- Tweaks
- Writing Cases
- Writing Placard Docs
Contributing (engineering)