Bedrock Flows
Design systems

Making your first component

The three hand-authored files behind every Bedrock Flows component — and the one file you never touch.

Every component in components/ (and every utility in utilities/) follows the same contract. You hand-author three files in design-system/<version>/components/<name>/, and the build generates the rest.

FileHand-authored?Purpose
component.cssyesThe component's source-of-truth styles. Concatenated into the generated style.css.
macro.njkyesA Nunjucks macro that emits the canonical markup.
variants.jsonyesNamed prop sets, rendered through the macro.
<variant>.htmlGENERATEDA standalone HTML page per variant. Never hand-author or edit these.

The walkthrough below uses the real button component from ziptility-v1.

1. component.css — the styles

The component's source-of-truth CSS. It's concatenated into the version's generated style.css at build time. Use tokens from global.css:

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px 16px;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-fg);
  font-family: inherit;
  font-size: 14px;
  font-weight: 500;
  line-height: 1.2;
  cursor: pointer;
  text-decoration: none;
  transition: background 120ms ease, border-color 120ms ease;
}

.btn:hover { border-color: var(--color-border); }

.btn--primary {
  background: var(--color-accent);
  color: var(--color-accent-fg);
}
.btn--primary:hover { background: var(--color-accent-hover); }

.btn--ghost {
  background: transparent;
  border-color: var(--color-border);
}

.btn--danger {
  background: var(--color-danger);
  color: #ffffff;
}

.btn[disabled],
.btn[aria-disabled='true'] {
  opacity: 0.5;
  cursor: not-allowed;
}

2. macro.njk — the markup

A Nunjucks macro that emits the component's canonical markup. The button macro builds btn / btn--<variant> classes, picks <a> vs <button> based on whether props.href is present, handles disabled, and emits props.label:

{% macro button(props={}) %}
{%- set variant = props.variant or 'default' -%}
{%- set tag = props.href and 'a' or 'button' -%}
{%- set classes = ['btn'] -%}
{%- if variant != 'default' -%}
  {%- set classes = classes.concat(['btn--' + variant]) -%}
{%- endif -%}
<{{ tag }}
  class="{{ classes | join(' ') }}"
  {%- if tag == 'a' %} href="{{ props.href }}"{% endif -%}
  {%- if tag == 'button' %} type="{{ props.type or 'button' }}"{% endif -%}
  {%- if props.disabled %} disabled aria-disabled="true"{% endif -%}
>{{ props.label or 'Button' }}</{{ tag }}>
{% endmacro %}

Both your flow pages and the generated variant HTML import from this one macro, so the markup has a single source of truth.

3. variants.json — the prop sets

Declares named prop sets. Each entry is rendered through the macro to produce a variant. The shape is { "macro": "<name>", "variants": [{ "name", "filename", "props" }, ...] }:

{
  "macro": "button",
  "variants": [
    {
      "name": "Default",
      "filename": "default",
      "props": { "label": "Button" }
    },
    {
      "name": "Primary",
      "filename": "primary",
      "props": { "label": "Continue", "variant": "primary" }
    },
    {
      "name": "Ghost",
      "filename": "ghost",
      "props": { "label": "Cancel", "variant": "ghost" }
    },
    {
      "name": "Danger",
      "filename": "danger",
      "props": { "label": "Delete", "variant": "danger" }
    },
    {
      "name": "Disabled",
      "filename": "disabled",
      "props": { "label": "Pending…", "variant": "primary", "disabled": true }
    }
  ]
}
  • macro — the macro name to call from macro.njk.
  • name — the display name (shown in storybook).
  • filename — the kebab-case base name for the generated <filename>.html. Keep these stable: they become URLs consumers depend on.
  • props — the object passed to the macro.

The file you never touch: <variant>.html

The <variant>.html files — default.html, primary.html, ghost.html, and so on — are GENERATED by scripts/generate-variants.mjs, which renders the macro with each variant's props.

Do not hand-author or edit them. They're overwritten on every build. To change a variant, edit macro.njk or variants.json instead.

Adding a new component

Create the folder with the three authored files; the generator picks it up on the next build:

design-system/<version>/components/<new-component>/
  component.css              the rules
  macro.njk                  a macro that emits canonical markup
  variants.json              { "macro": "...", "variants": [...] }

See Variants & auto-generation for how the build turns these three files into rendered variants, a bundled stylesheet, and a Storybook.

On this page