Foundation
Colours, type and elevation are CSS custom properties, declared once for light and once for dark. Redeclaring them is how you theme the kit; overriding a class stays available for the cases that need it.
The name
margo is Latin for the edge of something — a border, a rim, the margin of a page. It is the word English took "margin" from, and it is the whole design brief of the kit in one term: what separates things here is a line and the space around it.
So the components are drawn rather than filled. A Card is a border and a radius, not a slab of colour; a Button at rest is an outline; a Table is rows separated by rules. There is one border width in the entire system — 1.5px — and one border colour, which is why the interface reads as consistent before any component is involved.
--margo-border-width-2: 1.5px;
--margo-color-border: #cfcfcf;Everything else is restraint. Elevation exists but stays a hint rather than a drop shadow; the accent colour appears on what is active or focused and nowhere else; motion is short and reversible. The intent is a clean, modern surface that never raises its voice — decoration is what you add on purpose, not what the kit hands you by default.
Colours
Colours come in pairs: a surface and what goes on top of it. main is the page, neutral is what sits above it, and each has its on- counterpart for text and icons. Naming them by role rather than by hue is what lets the dark theme swap the values without a single component knowing.
| token | |
|---|---|
--margo-color-main | page surface, the background everything sits on |
--margo-color-on-main | text and icons over the page surface |
--margo-color-neutral | raised surface: pure white in light, pure black in dark |
--margo-color-on-neutral | text over the raised surface |
--margo-color-primary | accent, used by glows, gradients and active states |
--margo-color-primary-darken | the darker end of every primary gradient |
--margo-color-secondary | muted fill, as in chips |
--margo-color-medium | secondary text, the resting state of icons |
--margo-color-border | every border of the kit |
Scales
Type, weight, elevation and the single border width are tokens too. There is one border width on purpose: two would need a rule for when to use which, and the kit prefers to spend that decision elsewhere.
| token | |
|---|---|
--margo-font-family-primary | Nunito, the reading typeface |
--margo-font-family-secondary | Bebas Neue, the display typeface |
--margo-font-size-* | mc, xs, sm, base, md, lg, 2lg, xl, 2xl … 9xl |
--margo-font-weight-* | thin through black, mapped to the numeric weights |
--margo-border-width-2 | 1.5px, the single border width of the kit |
--margo-shadow-card | elevation of Card |
--margo-shadow-button | elevation of Button |
--margo-shadow-item | elevation of Item, on hover, focus and active |
--margo-shadow-chip | elevation of Chip |
--margo-code-* | the snippet palette: surface, bar, and one colour per token type |
--margo-layer-duration | how long layered surfaces take to arrive and leave |
--margo-layer-easing | the curve they travel on: fast to start, slow to settle |
Overriding a colour
Redeclare the variable after importing the kit. No build step, no plugin, no theme object: the components read the variable at paint time, so the new value reaches them and the utilities at once. This is the way to change something everywhere — a brand accent, a surface, the border colour.
/* after @import "margo-ui/css" */
:root {
--margo-color-primary: #4f8cff;
--margo-color-primary-darken: #2f6ad9;
}
.dark {
--margo-color-primary: #7aa9ff;
--margo-color-primary-darken: #4f8cff;
}Override both themes when you touch a colour that exists in both. Forgetting the .dark block is the usual way a brand accent ends up unreadable at night.
Overriding in one place only
Because they are inherited custom properties, the same override works scoped to a subtree: put it on a class or a data attribute and everything inside adopts it, including the components of the kit.
.margo-invoice {
--margo-color-primary: #e5a950;
--margo-color-border: #e0d3bb;
}How tokens reach the utilities
A second layer maps the --margo- variables onto Tailwind's theme. That is what makes bg-main, text-on-main, border-border and shadow-card exist as classes at all.
@theme {
--color-main: var(--margo-color-main);
--color-on-main: var(--margo-color-on-main);
--color-primary: var(--margo-color-primary);
--shadow-card: var(--margo-shadow-card);
--text-md: var(--margo-font-size-md);
}The indirection is deliberate. Overriding a --margo- variable changes both the kit and your own utility classes; overriding the Tailwind variable directly would change only the utilities, and the two would drift apart.
When a token is the wrong tool
Tokens are for decisions that hold across the interface. A single component that has to look different in one place is not one of those: there, pass a className and let cn merge it over the defaults. The kit is built for that too — it is why every component takes a className and why the merge exists at all.
The distinction is worth keeping: a token changes the system, a class changes an instance. Reaching for a token to fix one screen makes every other screen shift with it; reaching for a class to change a colour used in twenty places means twenty places to remember.
Adding tokens of your own
The same mechanism is open to your project: declare your own @theme block after the kit and you get utilities on the same footing as the built-in ones. This site does exactly that for the navbar height and the page offsets.
/* your own theme file, imported after the kit */
@theme {
--spacing-navbar: 3.5rem;
--spacing-page-offset: 1.5rem;
--z-index-navbar: 50;
}