This document defines the rules and conventions for building components in the By Default design system. All contributors must follow these patterns to keep the system consistent and predictable.
How this system is organized
Components are split across four layers so the design system can be lifted into other products without dragging the BrandOS docs site along with it:
foundation: tokens, layout primitives, utilities. Lives inassets/css/design-system.css. Ships with every product.core: reusable components (button, card, dropdown, …) plus brand identity docs (brand-*.md). Lives indesign-system.css. Ships with every product.docs-site: components that only power this BrandOS docs site (asset-card, book-cover, dont-card). Lives inassets/css/docs-site.css. Does not ship.app: BrandOS-specific tools, integrations, and project content (calculators, world clock, ad preview, project case studies). Does not ship.
Every cms/*.md doc declares its layer in frontmatter:
---
title: "Button"
section: "Design System"
layer: "core"
---
The layer field is required. The doc generator validates it on every build. See CLAUDE.md §17 (Layer Discipline) for the seven rules that govern this split. The one exemption: files starting with _ (e.g. _defaults.md) are generator config stubs, excluded from the build and from the layer requirement.
Naming convention
- Base class: (e.g. , , )
- Variation:
data-*attributes (e.g.data-color="success",data-variant="outline",data-type="info") - State classes: — the shared vocabulary is , , , , , , . marks selection (a chosen option in a group), distinct from (the current or engaged item). Components may add scoped states (e.g. , , ) documented in their own doc — but reach for the shared vocabulary first
- Utility overrides: use
!importantonly on utility classes (e.g. ) - JS hooks: use
data-*attributes, never CSS class names
CUBE pattern: Components use data-* attributes for variation and .is-* classes for transient state only. Role classes (, ) compose with the base via token overrides. See individual component docs for details.
Three axes of variation
All variation across components follows three axes. Each resolves to the same --status-* token layer underneath.
| Axis | Attribute | What it means | Values | Components |
|---|---|---|---|---|
| Colour | data-color |
Visual colour, no inherent message meaning | success/green, warning/yellow, danger/red, info/blue, accent/purple |
button, badge, tag |
| Type | data-type |
Semantic message meaning, the content IS this type | success, warning, danger, info, accent |
callout, toast |
| Variant | data-variant |
Visual shape/hierarchy | outline, faded, outline-faded, transparent, text (button); thumb (segmented control) |
button, segmented control |
The distinction: Display components (badge, tag, button) use data-color. The colour is visual emphasis. Feedback components (callout, toast) use data-type. The type describes what the message means. Both use the same value names (success, warning, danger, info, accent) and resolve to the same --status-* tokens. Rebranding is a token change, not an attribute change.
One recorded exception: the progress ring's status variants are BEM modifiers (), not data-color — matching progress.progress-bar--success on the same doc page. Consistency with its own sibling beats consistency with the axis table; do not copy the pattern to new components.
Token rule
Every visual value in component CSS must reference a CSS custom property defined in :root. Never hardcode hex values, pixel values (except structural ones like border-radius: 50%), or raw font values.
Component tokens follow this pattern:
--component-property: var(--semantic-token);
Example (button component):
--button-color: var(--text-primary);
--button-bg: var(--button-color);
--button-border: var(--button-color);
--button-text-color: var(--text-inverted);
Slot layout rule
Components that lay content out in slots — Site Header and Bar today — separate an outer element from an inner one and push the trailing slot over with an auto margin:
.component { /* sticky, background, border — full bleed */ }
.component-inner { display: flex; align-items: center; }
.component-end { margin-inline-start: auto; }
Never justify-content: space-between for this. With a single child it resolves to flex-start, so a bar carrying only a trailing slot renders at the leading edge — the exact opposite of what the markup says. The auto margin gets every combination right, and it means no slot is ever required to exist. If you find yourself emitting an empty <div> so the layout keeps its shape, the layout is wrong, not the markup.
The outer/inner split exists so a full-bleed background and border can run edge to edge while the contents line up with a content column.
File rule
| What | Where |
|---|---|
| Component CSS | assets/css/design-system.css under a numbered section heading |
| Component JS (if needed) | assets/js/component-name.js |
| Documentation source | cms/component-name.md |
| Generated docs page | website/component-name.html |
Section headings in design-system.css follow the format:
/* ------ 16. BADGE ------ */
Accessibility rule
Every interactive component must include:
| Requirement | Details |
|---|---|
| ARIA roles | Correct role attribute (e.g. role="tablist", role="tab", role="tabpanel") |
| ARIA attributes | aria-selected, aria-controls, aria-labelledby, aria-current, aria-label as needed |
| Keyboard support | Tab to focus, Enter/Space to activate, Escape to dismiss (where applicable), Arrow keys for navigation (tabs, menus) |
| Focus indicator | box-shadow: 0 0 0 2px color-mix(in srgb, var(--input-focus), transparent 75%) |
| Screen reader text | Use aria-label or visually hidden text for icon-only actions |
Component status
| Component | CSS class | Needs JS | Docs page |
|---|---|---|---|
| Accordion | , , | Yes (accordion.js) |
accordion.md |
| Button | No | button.md |
|
| Form elements | , , , , | No | form.md |
| Callout | No | callout.md |
|
| Disclosure | details/summary |
No | disclosure.md |
| Badge | No | badge.md |
|
| Card | No | card.md |
|
| Copy Button | Yes (copy-button.js) |
copy-button.md |
|
| Breadcrumb | No | breadcrumb.md |
|
| Tabs | , | Yes (tabs.js) |
tabs.md |
| Progress | , | No | progress.md |
| Tooltip | [data-tooltip] |
No | tooltip.md |
| Toast | Yes (toast.js) |
toast.md |
|
| Code / Pre / Kbd | code, pre, kbd |
No | code.md |
| Nav | , | No | nav.md |
| Site Header | , | No | site-header.md |
| Footer | , , , , | No | footer.md |
| Avatar | No | avatar.md |
|
| Bar | , , , , , , | Yes — bar.js (overflow) |
bar.md |
| Page Header | , , , , , | No | page-header.md |
| Section Header | , , | No | section-header.md |
| Login | , , , , , , , , | No | login.md |
| Cell Input | , , , | Yes — cell-input.js (cell build + paint) |
cell-input.md |
| App Navigation | , , , , | No | app-nav.md |
| Mark / Abbr / Figure | mark, abbr, figure |
No | mark.md |
| Skeleton | , , | No | skeleton.md |
Asset Card, Book Cover and Don't Card are documented separately as docs-site components. They only exist to power this BrandOS docs site and are not part of the portable design system. See Layer Discipline and CLAUDE.md §17.
Dark mode rule
Components must not contain dark-mode-specific CSS. They rely entirely on semantic token overrides in [data-theme="dark"] and @media (prefers-color-scheme: dark).
The only exception is when a component uses brand-palette tokens directly (avoid this). If unavoidable, add the override to both the [data-theme="dark"] block and the @media fallback block.
Current exceptions:
markelement: uses via token, requires dark mode override- Scrollbar: uses neutral scale tokens directly, requires dark mode override
How to add a new component
Define tokens in
:root(indesign-system.css, after existing component tokens):/* -- Component tokens -- */ --component-property: var(--semantic-token);Add dark mode overrides if the component uses non-semantic tokens. Add to both
[data-theme="dark"]and@media (prefers-color-scheme: dark)blocks.Write CSS in
design-system.cssunder a new numbered section:/* ------ N. COMPONENT NAME ------ */Write JS (only if needed) in
assets/js/component-name.js. Follow the existing pattern: IIFE, named functions, version logged to console.Write documentation in
cms/component-name.mdfollowing the standard frontmatter and content structure.Update this spec file: add the component to the status table above.
Regenerate docs:
cd cms/generator && npm run docgen
The website channel
The website is also a channel — one of the places the brand meets an audience — and the one where code, not export discipline, keeps it on-brand. Tokens carry the palette, type, spacing, and motion. If a page needs a value the tokens don't provide, the system is incomplete. Fix the tokens, not the page. The studio team owns this channel.
Identity here
- Logo: the primary centred lockup holds the top nav; the avatar covers favicons and compact contexts. Variants, files, and clearspace: 4.1 Logo.
- Colour and type: never chosen per page. Semantic tokens only: 4.2 Colour, 4.3 Typography. Embedded graphics and imagery follow the same palette.
- Motion: transitions and animation run on the motion tokens (Motion); no hardcoded durations or easings.
- Icons: brand icons only, prepared to system rules: 4.4 Iconography.
Specs
| Asset | Size | Format | Max weight | Notes |
|---|---|---|---|---|
| Open Graph image | 1200×630 (1.91:1) | JPG/PNG | 1 MB | unique per page, absolute URL in the meta tag |
| X card image | 1200×628 | JPG/PNG | 1 MB | summary_large_image; usually the OG image reused |
| Favicon | SVG source + 32×32 PNG + ICO | SVG/PNG/ICO | – | avatar logo variant; lives in assets/icons/ |
| Apple touch icon | 180×180 | PNG | – | assets/icons/apple-touch-icon.png |
| Page imagery | ratio set at render | – | – | + img-1x1 / img-3x2 / img-4x3 / img-16x9 / img-21x9, crop with the image component, not in the export |
Specs verified against platform documentation: 2026-07-05.
The full meta-tag set is already wired into templates/page-template.html; the per-tag reference lives in SEO best practices.
Templates & assets
- Page template: every new page starts from
templates/page-template.html; the SEO head, layout hierarchy, and script order are already correct there. - Default OG image:
assets/images/og/og-default.jpg, the fallback when a page has no bespoke share image. Bespoke OG images live alongside it inassets/images/og/. - Draft imagery: the placeholder service returns an SVG at any size:
https://bydefault.design/image/{width}x{height}. Use it while layouts settle; replace before ship.
Checklist
- Unique title (50–60 characters) and meta description (150–160)
- OG image present at 1200×630, absolute URL, unique to the page
- Favicon set referenced in the head
- Zero hardcoded values: colour, type, spacing, and motion all from tokens
- Imagery cropped by ratio classes, alt text on everything
- Keyboard navigation works; WCAG 2.1 AA holds; Lighthouse 90+