The design system consists of design tokens expressed as css custom properties. The design tokens are then organized into two main layers, foundation and semantic.
Foundation tokens are the smallest building blocks. They are primitive values and their describe what they are. For example —radius-1 is the a value that should be used to specify, well how rounded the corners of an element should be.
They serve an important purpose, they constrain the possible values from anything to a subset of pre-defined ones. This both speeds up the decision process and improves consistency through the ui.
The foundational tokens in this design system are an adaptation of the fantastic open props
The semantic tokens is an abstraction on top of the foundation tokens that
should encode a pattern that should be applied across multiple elements
like —color-text.
A good rule of thumb is that themes should only change semantic layer tokens. This also gives a good indication on whether a token is foundational or semantic.
By default there are two themes setup; light and dark. These mainly changes the colors from light to dark. But there could be more themes that changes other custom properties in the semantic layer like input radius or spacing. These could even complement each other. For example having theme “light” and “spacious” applied at the same time (if a “spacious” theme was created).
For more info about the color setup in the themes see the Color system
Next: Color system
As mentioned the semantic layer deals quite a bit with colors.
To make it easier to use accessible colors there is a color system in place that helps with generating pallets.
From 4 seed colors: neutral, primary, destructive and constructive 4 swatches are generated with the same names.
| Softer | Soft | Mid | Loud | Louder | |
|---|---|---|---|---|---|
| Fill | |||||
| Text | |||||
| Stroke |
| Softer | Soft | Mid | Loud | Louder | |
|---|---|---|---|---|---|
| Fill | |||||
| Text | |||||
| Stroke |
| Softer | Soft | Mid | Loud | Louder | |
|---|---|---|---|---|---|
| Fill | |||||
| Text | |||||
| Stroke |
| Softer | Soft | Mid | Loud | Louder | |
|---|---|---|---|---|---|
| Fill | |||||
| Text | |||||
| Stroke |
When picking seed colors use a neutral color any color in the 500-range on https://color.surf/ should be fine.
The system starts with four seed colors that define the base hue for each color family:
--color-primary-seed - Main brand color--color-neutral-seed - Grayscale colors--color-constructive-seed - Success/positive actions--color-destructive-seed - Error/negative actionsEach seed color is automatically expanded into an 11-step scale (50-950) using color-mix() in oklab color space:
The scale values are then mapped to semantic tokens that describe their purpose. These mappings flip between light and dark themes to maintain proper contrast.
Below is a list of all foundation tokens.
5px
1rem
4rem
8rem
-.5rem
-.25rem
.25rem
.5rem
1rem
1.25rem
1.5rem
1.75rem
2rem
3rem
4rem
5rem
7.5rem
10rem
15rem
20rem
30rem
20ch
45ch
60ch
.5rem > 1vw < 1rem
1rem > 2vw < 1.5rem
1.5rem > 3vw < 2rem
2rem > 4vw < 3rem
4rem > 5vw < 5rem
5rem > 7vw < 7.5rem
7.5rem > 10vw < 10rem
10rem > 10vw < 15rem
15rem > 30vw < 20rem
20rem > 40vw < 30rem
The quick brown fox jumps over the lazy dog
IBM Plex Mono (body/code font)
The quick brown fox jumps over the lazy dog
Departure Mono (display font)
The quick brown fox
400
The quick brown fox
500
The quick brown fox
700
The quick brown fox jumps over the lazy dog. The five boxing wizards jump quickly.
.95
The quick brown fox jumps over the lazy dog. The five boxing wizards jump quickly.
1.1
The quick brown fox jumps over the lazy dog. The five boxing wizards jump quickly.
1.25
The quick brown fox jumps over the lazy dog. The five boxing wizards jump quickly.
1.375
The quick brown fox jumps over the lazy dog. The five boxing wizards jump quickly.
1.5
The quick brown fox jumps over the lazy dog. The five boxing wizards jump quickly.
1.75
The quick brown fox jumps over the lazy dog. The five boxing wizards jump quickly.
2
The quick brown fox
-.05em
The quick brown fox
.025em
The quick brown fox
.050em
The quick brown fox
.075em
The quick brown fox
.150em
The quick brown fox
.500em
The quick brown fox
.750em
The quick brown fox
1em
Semantic tokens come in all shapes and sizes, below is an overview of all the values.
For ui elements the typescale is limited to 4
sizes. --h1,--h2,--p and --p2. This combined with text color
provides enough visual hierarcy for normal ui.
Long form text like the documentation you’re reading now might need a
bigger typescale and for that usecase .typesetting exists,
see #doc-css-restyle
The type scale provides consistent sizing across all text elements:
2rem bold / 1.1 (Departure Mono)
1.5rem medium / 1.1 (IBM Plex Mono)
Body text for paragraphs and general content.
1rem regular / 1.4 (IBM Plex Mono)
Small text for captions and secondary information.
0.875rem regular / 1.4 (IBM Plex Mono)
.box class that can be used, but for
other places where box styling is disired —radius-box can be used
There are a lot of input elements. They should all have consistent styling.
These custom properties are used to style both the custom elements like
<m-input> as well as the native <input/> elements
In adition to all the stroke variants of colors (like
--color-primary-stroke), that can be used to set the border color, there
are other custom propertise that should be used for consistent border
styling
border: var(--border)
--border but the strong variant.
The color system adapts to light and dark themes. Each color is defined with multiple variants for different use cases.
The color system includes four semantic scales: primary, neutral, constructive, and destructive. Each scale has consistent variants:
The .typesetting class creates consistent vertical rhythm for text-heavy content using CSS Grid. It styles headings, paragraphs, code blocks, horizontal rules, and maintains proper spacing between all elements.
1rlh gapApply .typesetting to containers with:
This is a paragraph demonstrating body text flow with consistent spacing and readability.
Lorem ipsum with some inline code showing proper vertical rhythm.
function example() {
return "Code blocks styled nicely";
}Final paragraph showing consistent vertical rhythm in action.
This is a paragraph demonstrating body text flow without proper spacing.
Lorem ipsum with some inline code showing inconsistent spacing.
function example() {
return "Code blocks cramped";
}Final paragraph showing poor vertical rhythm.
Utilities for spacing, borders, and layout using data attributes. All utilities use logical properties for better internationalization support.
CSS utilities use data attributes instead of classes. Write data-padding="3" instead of .p-3, or data-background="surface" instead of .bg-surface.
All utility values map to design system tokens.
Spacing utilities (margin, padding), borders, gaps, and alignment attributes support space-separated values:
<div data-padding="i-5 b-3">
This applies padding-inline: var(--size-5) and padding-block: var(--size-3).
<div data-margin="i-auto b-4">
This applies margin-inline: auto and margin-block: var(--size-4).
Spacing and border utilities use logical property suffixes:
-i — inline (left and right in LTR languages)-is — inline-start (left in LTR, right in RTL)-ie — inline-end (right in LTR, left in RTL)-b — block (top and bottom)-bs — block-start (top)-be — block-end (bottom)Example: data-padding="i-5" applies horizontal padding, while data-padding="b-3" applies vertical padding.
Add borders to elements using the [data-border] attribute. Supports directional variants and combinations.
i — inline (left and right)is — inline-start (left in LTR)ie — inline-end (right in LTR)b — block (top and bottom)bs — block-start (top)be — block-end (bottom)Combine directions:
<div data-border="bs be"> <!-- top and bottom -->
Use [data-variant] to change border color:
Text color utilities apply semantic text colors using the [data-text] attribute.
[data-text=""] (default body text)
[data-text=“mid”] (secondary text)
[data-text=“softer”] (tertiary text)
Use these values when text appears on colored fills to ensure proper contrast:
[data-text="on-{family}-soft"] — Text on soft backgrounds[data-text="on-{family}-mid"] — Text on mid backgrounds[data-text="on-{family}-loud"] — Text on loud backgroundsWhere {family} is primary, neutral, constructive, or destructive.
Apply background colors using the [data-background] attribute. Color families include five intensity levels: softer, soft, mid, loud, and louder.
Use [data-text] for proper text contrast on colored backgrounds.
[data-background]
[data-background=“surface”]
[data-background=“surface-raised”]
[data-background="primary-softer"] [data-text="on-primary-soft"]
[data-background="primary-soft"] [data-text="on-primary-soft"]
[data-background="primary-mid"] [data-text="on-primary-mid"]
[data-background="primary-loud"] [data-text="on-primary-loud"]
[data-background="primary-louder"] [data-text="on-primary-loud"]
[data-background="neutral-softer"] [data-text="on-neutral-soft"]
[data-background="neutral-soft"] [data-text="on-neutral-soft"]
[data-background="neutral-mid"] [data-text="on-neutral-mid"]
[data-background="neutral-loud"] [data-text="on-neutral-loud"]
[data-background="neutral-louder"] [data-text="on-neutral-loud"]
[data-background="constructive-softer"] [data-text="on-constructive-soft"]
[data-background="constructive-soft"] [data-text="on-constructive-soft"]
[data-background="constructive-mid"] [data-text="on-constructive-mid"]
[data-background="constructive-loud"] [data-text="on-constructive-loud"]
[data-background="constructive-louder"] [data-text="on-constructive-loud"]
[data-background="destructive-softer"] [data-text="on-destructive-soft"]
[data-background="destructive-soft"] [data-text="on-destructive-soft"]
[data-background="destructive-mid"] [data-text="on-destructive-mid"]
[data-background="destructive-loud"] [data-text="on-destructive-loud"]
[data-background="destructive-louder"] [data-text="on-destructive-loud"]
Control spacing between flex or grid items using the [data-gap] attribute.
Use directional prefixes for independent row and column spacing:
r- — row-gap (vertical spacing)c- — column-gap (horizontal spacing)Combine row and column gaps:
<div data-gap="r-3 c-5"> <!-- row-gap: var(--size-3); column-gap: var(--size-5) -->
Available sizes: 0, 000, 00, 1-15, fluid-1 through fluid-10
Add space outside elements using the [data-margin] attribute.
Combine directions:
<div data-margin="i-5 b-3"> <!-- margin-inline: var(--size-5); margin-block: var(--size-3) -->
Available sizes: 0, 000, 00, 1-15, fluid-1-10, auto
Add space inside elements using the [data-padding] attribute.
Combine directions:
<div data-padding="i-body b-3"> <!-- padding-inline: var(--body-margin); padding-block: var(--size-3) -->
Available sizes: body, 000, 00, 1-15, fluid-1-10
Control flexbox and grid alignment using [data-align] and [data-justify] attributes.
Controls vertical alignment in flex layouts and block-axis alignment in grid.
align-items (default):
start, center, end, baseline, stretchalign-content (prefix: content-):
content-start, content-center, content-endcontent-between, content-around, content-evenlycontent-baseline, content-stretchUse -items to align individual items. Use content- when working with wrapped flex items or grid tracks.
Controls horizontal alignment in flex layouts and inline-axis alignment in grid.
justify-items (default):
start, center, end, stretchjustify-content (prefix: content-):
content-start, content-center, content-endcontent-between, content-around, content-evenlyUse -items to align individual items. Use content- when working with wrapped flex items or grid tracks.
Combine values within each attribute:
<div data-align="center content-start"> <!-- align-items + align-content -->
<div data-justify="start content-between"> <!-- justify-items + justify-content -->
Combine both attributes for full control:
<div data-align="center" data-justify="content-between">
Controls main-axis alignment of items within their grid areas.
Controls cross-axis alignment of items within their grid areas.
A set of layout primitives that can be used to build interfaces with.
Flexible grid layout with configurable columns and spanning capabilities.
.grid — creates a grid layout (defaults to 1 column)[data-cols="1-12"] — set number of columns[data-span="1-12"] — span multiple columns[data-row-span="1-12"] — span multiple rowsCombine with [data-gap] for spacing between grid items,
or combine with [data-justify] and [data-align] for cell alignment.
A 3-column grid with equal-width columns.
<div class="grid" data-cols="3" data-gap="2">
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2</div>
<div class="box" data-variant="surface">Item 3</div>
</div> Items can span multiple columns using data-span.
<div class="grid" data-cols="3" data-gap="2">
<div class="box" data-variant="surface" data-span="2">Spans 2 columns</div>
<div class="box" data-variant="surface">Item</div>
</div> Items can span both rows and columns simultaneously.
<div class="grid" data-cols="3" data-gap="2">
<div class="box" data-variant="surface" data-row-span="2">Spans 2 rows</div>
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2</div>
<div class="box" data-variant="surface" data-span="2">Spans 2 columns</div>
</div> Responsive grid layout that automatically adapts to available space. Items wrap to new rows as needed, maintaining a minimum card width.
.collection — responsive grid (defaults to --size-fluid-8 minimum width)[data-size="1-10"] — adjust minimum card width using fluid size scale[data-variant="fit"] — collapse empty columns and stretch items to fill available spaceBy default, Collection uses auto-fill which creates as many columns as will fit, leaving empty space if items don’t fill the row. Use data-variant="fit" to collapse empty columns and stretch items to fill the container.
Combine with [data-gap] for spacing between items.
Default collection with automatic column creation based on available space.
<div class="collection">
<div class="box" data-variant="surface">Card 1</div>
<div class="box" data-variant="surface">Card 2</div>
<div class="box" data-variant="surface">Card 3</div>
</div> Use data-size to control minimum card width. Larger sizes create wider cards that wrap earlier.
<div class="collection" data-size="7" data-gap="3">
<div class="box" data-variant="surface">Card 1</div>
<div class="box" data-variant="surface">Card 2</div>
<div class="box" data-variant="surface">Card 3</div>
<div class="box" data-variant="surface">Card 4</div>
</div> With data-variant="fit", empty columns collapse and items stretch to fill available space.
<div class="collection" data-variant="fit" data-gap="3">
<div class="box" data-variant="surface">Card 1</div>
<div class="box" data-variant="surface">Card 2</div>
<div class="box" data-variant="surface">Card 3</div>
</div> Full-width grid layout with centered content area and optional breakout zones.
.content-grid — creates three-column grid (full-width, breakout, content)[data-width="breakout"] — wider than content, but not full-width[data-width="full"] — spans entire viewport widthControl content grid widths with CSS custom properties:
--content-width — maximum width for default content (default: 1600px)--content-breakout-width — maximum width for breakout content (default: 1800px)--body-margin — inline padding for all content (default: var(--size-fluid-2))Use Content Grid for article layouts, documentation pages, or any content where most elements should be constrained to a readable width, but some elements (like images or code blocks) need to break out wider or span the full viewport.
Content grids can be nested to create different content width zones within a page.
Children default to content width. Use data-width to break out to wider zones.
<div class="content-grid" data-gap="2">
<div class="box" data-variant="surface">Content width (default)</div>
<div class="box" data-variant="surface" data-width="breakout">Breakout width</div>
<div class="box" data-variant="surface" data-width="full">Full width</div>
</div> Override width variables for specific content grids.
<div class="content-grid" style="--content-width: 200px; --content-breakout-width: 300px;">
<div class="box" data-variant="surface">Narrower content (800px max)</div>
<div class="box" data-variant="surface" data-width="breakout">Narrower breakout (1000px max)</div>
</div> Content grids can be nested to create sections with different width constraints.
<div class="content-grid">
<div class="box" data-variant="surface">Outer content width</div>
<div class="box content-grid" style="--content-width: 200px; --content-breakout-width: 300px;" data-width="full">
<div class="box" data-variant="surface">Nested content grid with narrower width</div>
<div class="box" data-variant="surface" data-width="breakout">Nested content grid with narrower width</div>
</div>
<div class="box" data-variant="surface">Back to outer content width</div>
</div> Flex layout for arranging items in a single direction with optional spacing and alignment.
.stack — vertical stack (column, default)[data-direction="row"] — horizontal stackCombine with [data-gap] for spacing. Control alignment with:
[data-align] — cross-axis alignment (horizontal in column, vertical in row)[data-justify] — main-axis alignment (vertical in column, horizontal in row)Use Stack when you need a single column or row of items with consistent spacing. For multi-dimensional layouts where items wrap, use Collection instead.
Default stack direction is vertical (column).
<div class="stack">
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2</div>
</div> Add spacing between stacked items.
<div class="stack" data-gap="3">
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2</div>
</div> Use data-direction="row" for horizontal layout.
<div class="stack" data-direction="row">
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2</div>
</div> Combine direction and gap for horizontal spacing.
<div class="stack" data-direction="row" data-gap="3">
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2<br>More content</div>
</div> In horizontal stacks, data-align controls vertical alignment.
<div class="stack" data-direction="row" data-gap="3" data-align="start">
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2<br>More content</div>
</div> <div class="stack" data-direction="row" data-gap="3" data-align="end">
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2<br>More content</div>
</div> <div class="stack" data-direction="row" data-gap="3" data-align="stretch">
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2<br>More content</div>
</div> In horizontal stacks, data-justify controls horizontal alignment.
<div class="stack" data-direction="row" data-gap="3" data-justify="center">
<div class="box" data-variant="surface">Item 1</div>
<div class="box" data-variant="surface">Item 2<br>More content</div>
</div>