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.
| File | Hand-authored? | Purpose |
|---|---|---|
component.css | yes | The component's source-of-truth styles. Concatenated into the generated style.css. |
macro.njk | yes | A Nunjucks macro that emits the canonical markup. |
variants.json | yes | Named prop sets, rendered through the macro. |
<variant>.html | GENERATED | A 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 frommacro.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.