Design System
design is where your app's design system lives. It holds the tokens — the same ones theme has always held — plus everything a design system needs that tokens alone cannot carry: the principles behind them, the mark it is built around, the voice the app speaks in, what each colour is for, the ladder its type is set on, the register its images work in, and what each component is for.
That last part is the point. A palette of hex values tells a renderer everything and a human nothing. primary: '#3b5bdb' does not say whether it is a CTA fill or a body-text colour, so anyone adding a page later — a teammate, or an AI agent working in your repo — picks by feel and the design drifts. design makes those rules declarable, and sovrium design-system hands them over as a file.
design:
theme:
colors:
primary: '#3b5bdb'
primary-fg: '#ffffff'
principles:
- Restraint over ornament
- Colour is spent on error, and nowhere else
logo:
src: /logos/wordmark.svg
srcDark: /logos/wordmark-light.svg
alt: Acme
minWidth: 96px
misuse:
- Never re-colour the mark.
typeScale:
h1:
size: '3rem'
lineHeight: 1.1
weight: 700
body:
size: '1rem'
lineHeight: 1.6
imagery:
iconSet: Lucide
photography:
- No stock photography.
voice:
personality: [warm, direct, never condescending]
pronoun: you
prefer:
- 'Lead a CTA with its verb: Deploy, Save, Delete.'
- Every empty state carries a guidance line naming the next action.
avoid:
- No exclamation marks in product chrome.
- No emoji.
tone:
empty: Say what this is, then the one next action.
loading: Say how long, and that they can leave.
error: State the constraint, then offer two ways forward. Never accuse.
success: One line, ending in a period. No fireworks.
destructive: Name what is deleted, how many, and whether it is reversible.
colorRoles:
primary:
usage: Primary CTA fill only. Never body text, never a status pill.
pairsWith: primary-fg
components:
section-header:
usage: A titled band introducing a page section.
when: Use above any section carrying more than three children.
dont: Never nest one inside another — the heading levels collide.design.theme and the theme alias
design.theme is the canonical home for design tokens. It accepts exactly what the top-level theme key accepts — every category, unchanged: colors, darkColors, fonts, spacing, shadows, borderRadius, breakpoints, animations, baseline, colorScheme, codeBlock.
Top-level theme still works. It is a supported alias, and every existing config keeps running untouched.
Declaring both is an error, not a merge. If theme and design.theme are both present, sovrium validate refuses the config and names the fix. A merge would have to pick a winner, and whichever side lost would take effect nowhere with nothing said — so Sovrium refuses instead of guessing.
# Canonical
design:
theme:
colors:
primary: '#3b5bdb'
# Still supported — a deprecated alias
theme:
colors:
primary: '#3b5bdb'sovrium validate prints a deprecation notice when it sees the top-level form, on stderr, and still exits 0. The alias is removed in the next major version.
Principles
design.principles is an ordered list of the convictions behind your tokens. They render at the top of the export, so whoever reads it gets the reasoning before the values.
design:
principles:
- Restraint over ornament
- The visitor is the hero, not the productLogo
design.logo holds the mark and the rules for placing it — the first section of every brand charter.
design:
logo:
src: /logos/wordmark.svg
srcDark: /logos/wordmark-light.svg
alt: Acme
clearSpace: Leave clear space equal to the height of the mark on all four sides.
minWidth: 96px
misuse:
- Never re-colour the mark.
- Never stretch, rotate or add effects.
- Never place it on a busy photograph without a plate.| Field | Notes |
|---|---|
src |
Required. The primary mark, as it appears on the app's default (light) surface. |
srcDark |
The variant shown when the interface is in dark mode. See the warning below. |
alt |
Required. The accessible name — normally just the app's name. |
clearSpace |
The exclusion zone, in prose. |
minWidth |
The smallest width the mark may be reproduced at: a number with px or rem. |
misuse |
What must never be done to the mark. |
alt is required alongside src because a mark with no accessible name is a defect rather than a partial declaration — it reaches a screen reader as nothing at all. Write the app's name, not logo: a screen reader already announces the element as an image.
clearSpace is deliberately prose and not a dimension. Every charter worth reading states clear space relative to the mark — "the height of the S on all four sides" — because that is the rule that survives the mark being resized. minWidth is a dimension because, unlike clear space, it genuinely is one number, and it is the number a consumer can enforce.
misuse is the half that changes behaviour. "Use the wordmark" tells a designer nothing they were not already going to do; "never re-colour it, never set it on a busy photograph" is the sentence that prevents the thing you did not want.
srcDark is named for the MODE, not for the ink. This is the field most often filled in backwards, because logo files are usually named for their ink and the two conventions are opposites. A dark-ink file is the one shown in light mode, so it belongs in src; the light-ink file belongs in srcDark. Omit srcDark entirely when one mark reads on both surfaces — requiring a second file invites a duplicate that then goes stale.
Where the file lives
src and srcDark hold a reference, not bytes, and the only thing any consumer can do with one is put it in a src= attribute. Two forms are accepted:
- Root-relative —
/logos/wordmark.svg. This covers both storage homes with one rule: a file in the app's public directory is served at/name.ext, and a bucket object at/api/buckets/{bucket}/files/{path}. - Absolute
https://—https://cdn.example.com/wordmark.svg, for a mark on a CDN or owned by another party.
Everything else is refused, each for a reason:
| Refused | Why |
|---|---|
A bare relative path (wordmark.svg) |
It resolves against the current page, so the same declaration loads /wordmark.svg on the home page and /fr/docs/guide/wordmark.svg inside the docs zone. There is no way to write a correct one, so accepting one accepts only mistakes. |
http:// |
An https app loading an http image is mixed content: the browser blocks it silently and the mark is simply absent. |
data: |
Inlining a wordmark into every page's HTML is paid for on every request and cached on none. |
Prefer SVG. A wordmark is line art: resolution-independent, smaller than any raster encoding of it, sharp at every density, and with no codec question at all. The AVIF rule that governs committed imagery is about screenshots, not marks — a customer's wordmark is their trademark, and re-encoding it is not Sovrium's call. A mark declared here is served verbatim and never enters the runtime image-transform pipeline.
Voice
design.voice describes how your app speaks, whatever the situation. Every field is optional.
| Field | Type | What it holds |
|---|---|---|
personality |
string[] |
Traits that hold everywhere: ['warm', 'direct', 'never condescending']. |
pronoun |
string |
How the app addresses the reader — tu, vous, you, Sie. |
prefer |
string[] |
Patterns to reach for, written as instructions. |
avoid |
string[] |
Patterns to refuse. |
tone |
object |
How the register shifts per situation — see below. |
pronoun is a free string rather than a fixed list, because the register contract is per-language: an app written in French chooses between tu and vous, one written in English has no choice to make, and a fixed list would refuse the first language it never enumerated.
prefer / avoid are the two halves of a house style guide. Put the work into avoid: refusals are what stop plausible-looking off-brand copy.
Tone
design.voice.tone covers the five moments where a system speaks. Each value is one instruction telling the writer how to write that moment, not the literal string: the strings are per-locale and live in languages.
| Key | The moment |
|---|---|
empty |
A table with zero rows, a feature never used. |
loading |
A long-running task the reader is waiting on. |
error |
Anything that stops the reader: validation, network, refusal. |
success |
A save confirmed, a deploy finished. |
destructive |
A confirmation that must name its consequence. |
Declare the moments you have thought about and omit the rest. An absent key is better than an instruction invented to fill the table.
Colour roles
design.colorRoles answers the question theme.colors cannot: what is this colour for?
design:
theme:
colors:
primary: '#3b5bdb'
primary-fg: '#ffffff'
colorRoles:
primary:
usage: Primary CTA fill only. Never body text.
pairsWith: primary-fgusage says what it is for and — more usefully — what it is not for. pairsWith names the token this one is designed to sit against, usually its foreground companion, so a reader does not have to re-measure the contrast.
Every key must name a colour declared in your palette. A role documenting a token that does not exist is guidance nobody can act on, and in practice it is a typo — so sovrium validate refuses it and lists the tokens that do exist. pairsWith is deliberately not checked the same way: a legitimate companion is often a platform role token your app never redeclared.
Type scale
design.typeScale is the ordered ladder your type is set on — twelve named steps from display down to overline, each with its size, leading, weight and tracking.
design:
typeScale:
h1:
size: '3rem'
lineHeight: 1.1
weight: 700
letterSpacing: '-0.02em'
body:
size: '1rem'
lineHeight: 1.6It is the one key under design that emits CSS: every declared step becomes a --text-{step} custom property with its modifiers, and a text-{step} Tailwind utility. That is what makes it the working replacement for the three inert theme.fonts fields named at the end of this page.
See Type Scale for the full ladder, the unit rules, and what the export does with it.
Imagery
design.imagery covers how the app looks where tokens cannot reach. Two pages can use identical tokens and still look like two different products, because one chose a stock photograph of people pointing at a whiteboard and the other chose a screenshot of the running app.
design:
imagery:
principles:
- Show the product working, never a metaphor for it.
- A person in an image is doing their job, not posing.
photography:
- No stock photography.
- Natural light only — no colour grading toward a brand hue.
- Screenshots are captured at 2x on a neutral background.
iconSet: Lucide
patterns:
- A single paper-grain texture at 4% opacity, never more than one surface per screen.
- Illustrations are single-weight line art in the signature colour, never filled.| Field | Type | What it holds |
|---|---|---|
principles |
string[] |
The register images work in, as convictions. The imagery counterpart of design.principles. |
photography |
string[] |
The concrete rules a person applies while choosing or shooting. |
iconSet |
string |
The name of the one icon set every icon is drawn from. |
patterns |
string[] |
Non-photographic marks: texture, illustration style, background geometry. |
principles and photography are separate because a principle is a standard to judge against and a rule is a constraint to obey. "Show the product working" is a principle; "no stock photography, ever" is a rule, and conflating them makes the rule sound negotiable.
iconSet is the highest-leverage field here. Icon drift is not caused by anyone choosing a bad icon — it is caused by three authors each choosing a reasonable icon from three different sets, after which no amount of token discipline makes the toolbar look like one product. Naming the set once removes the decision. It is a name, not a URL or a package specifier: what a reader needs is which set to search, and a version pin answers a different question and goes stale on every bump.
Nothing in imagery names a file, deliberately. A logo is one specific artifact with one URL; imagery is a class of artifact with rules, and the images themselves are declared where they are used — in pages, in records, in buckets. An asset list here would look like it did something and would in fact be a second, unrendered copy going stale the moment either side changed.
Component guidance
design.components gives your own reusable components something to say beside their name.
components:
- name: section-header
type: container
children:
- type: text
element: h2
content: $title
design:
components:
section-header:
usage: A titled band introducing a page section.
when: Use above any section carrying more than three children.
dont: Never nest one inside another — the heading levels collide.Three fields, three different questions: usage is what it is, when is the situation that selects it over its neighbours, and dont is the misuse to refuse. All optional. Every key must name a declared components[].name — guidance attached to a component that does not exist renders nowhere, and the usual cause is a rename that updated one side and not the other.
Exporting it
The whole point of declaring this is to be able to hand it over. One generator produces all three surfaces, so they cannot disagree about what your design system is.
sovrium design-system
# The agent brief, on stdout
sovrium design-system app.yaml
# Committed beside the config, referenced from your agent instructions
sovrium design-system app.yaml --output DESIGN.md
# The token document, for tooling
sovrium design-system app.yaml --format json --output tokens.jsonMarkdown is the default because the default reader is a model. The command runs offline — no server, no database — so it fits a pre-commit hook or a CI step. It refuses an unknown --format and refuses a config that fails validation, rather than exporting a design system describing an app that cannot boot.
The endpoints
Both are admin-gated and read-only, and return 404 to anyone else.
| Endpoint | Format |
|---|---|
GET /api/admin/design-system.json |
W3C Design Tokens (DTCG) 2025.10 document. |
GET /api/admin/design-system.md |
The same brief the CLI prints. |
The Design System Console renders the same content as a page a person reads, and can publish it behind a revocable link to someone with no login.
In the JSON document, tokens appear as standard DTCG types — colours as {colorSpace, components, hex} objects, dimensions as {value, unit} — the type scale as typography composite tokens, and the Sovrium-specific layer (principles, logo, voice, colour roles, imagery, component guidance) rides in $extensions under the key com.sovrium.design-system, which is exactly what DTCG reserves $extensions for.
What the export deliberately leaves out of the token tree
Some values have no faithful DTCG form, and a malformed token is worse than an absent one — a tool would act on it. Those keep their raw text under $extensions instead of being bent into a standard type:
| Value | Why |
|---|---|
theme.shadows.* |
DTCG shadows are decomposed into color/offsetX/offsetY/blur/spread; a raw CSS box-shadow string cannot be parsed back into those reliably. |
A spacing value like clamp(1rem, 2vw, 3rem) |
A DTCG dimension is one number and one unit. |
theme.darkColors |
DTCG has no mode/scheme concept in this version. |
Three superseded theme.fonts fields
These validate, and then nothing reads them. They are called out here, and named in the export under $extensions, so you do not maintain a value that has no effect:
| Field | What actually happens |
|---|---|
theme.fonts.*.lineHeight |
Reaches nothing. No CSS custom property is emitted for it. |
theme.fonts.*.size |
Reaches only the legacy hero section renderer, as an inline font size. It never becomes a CSS variable. |
theme.fonts.*.weights |
Only the first entry is read, and only by that same hero renderer. It never reaches an @font-face rule, so weights: [300, 400, 700] loads no additional font file. |
Declare sizes, leading, weight and tracking in design.typeScale instead, where each step emits a real CSS custom property and a usable text-{step} utility. To load several weights of a font, declare the variants you need in theme.fonts explicitly.
sovrium validate prints a Superseded: notice naming each declared path, and exits 0. Superseded, not deprecated: "deprecated" means this worked and is going away, and these three never worked. They keep decoding until the next major, alongside the top-level theme alias — refusing them now would stop a booting app in exchange for zero rendering change. Nothing is translated for you either: typeScale is per step where theme.fonts is per face, so an automatic mapping would have to guess which face is which rung of the ladder.
Related Pages
- Theme Overview & Colors — the token categories in full.
- Type Scale —
design.typeScalein full. - Typography —
theme.fonts, the faces a type step is set in. - Design System Console — the console page and the revocable share link.
- Validation & Schema Generation —
sovrium design-systembesidevalidateandschema. - Admin Dashboard — the admin-gated read API.
- App Schema Overview — every root property.
Last updated August 28, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.