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.
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.
There are few of them, and that is the point. A palette wide enough to give every component its own colour is a palette that makes the interface look assembled from parts; reusing the same handful everywhere is what makes it read as one surface. The accent is the scarcest of all — spend it on what is active or focused, and the eye goes there because nothing else is asking.
| 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-low | the lowest contrast step above the surface: on-main dissolved into main |
--margo-color-low-alpha | the same step left translucent, for fills over anything but main |
--margo-color-medium | the step above it: muted text, the resting state of icons |
--margo-color-border | every border of the kit |
Two of them are not values but derivations: low is on-main dissolved into main, and low-alpha is the same quantity left translucent. Over the page they look identical; over a raised surface or an image only the second lets what is behind it through.
:root {
--margo-color-low: color-mix(in srgb, var(--margo-color-on-main) 4%, var(--margo-color-main));
--margo-color-low-alpha: color-mix(in srgb, var(--margo-color-on-main) 4%, transparent);
}
.dark {
--margo-color-low: color-mix(in srgb, var(--margo-color-on-main) 2.5%, var(--margo-color-main));
--margo-color-low-alpha: color-mix(in srgb, var(--margo-color-on-main) 2.5%, transparent);
}Being derived is the point: retheming main and on-main carries them along instead of leaving a fixed grey behind, which is how a quiet fill ends up matching the border by accident. The quantity is small and differs between the themes — a dark surface separates from what sits on it with far less light than a light one needs darkness.
The border and the corner
There is one border width in the system, and it is a token like the colours are. One on purpose: two would need a rule for when to use which, and the kit prefers to spend that decision elsewhere. It reaches the components as border-margo, which behaves like any Tailwind border utility and accepts the usual sides — border-t-margo and the rest.
Corners take one value too, but they need more than one size, so the token names a maximum and the scale divides it. rounded-margo-base is the whole of --margo-radius, rounded-margo-sm, rounded-margo-xs and rounded-margo-mc three quarters, a half and a quarter of it. Surfaces and controls take the whole, small controls three quarters, inline marks a half. Retune the token and every corner in the kit moves with it, holding the ratios — which is what a scale buys you over three unrelated lengths.
The names read the same way the size and weight scales do, so the scale can gain a step without touching the ones already in use. What needs a circle rather than a step on the ramp — Toggle, Ripple — stays on rounded-full and outside the scale. Tailwind's own rounded-* and border-* utilities are untouched and still available. Type, elevation and timings are tokens too, declared beside these and documented where they are used — the typefaces under Typography, the elevations on the components that carry them.
| token | |
|---|---|
--margo-radius | 0.5rem, the largest radius of the kit; the scale divides it |
--margo-border | 1.5px, the single border width of the kit |
Light and dark
Both themes ship with the kit, and neither is a separate stylesheet. The light values are declared on :root and the dark ones again under .dark, so the theme is whether that one class sits on the html element — components read the variables at paint time and never learn which one is on. Only what differs is redeclared: colours and elevations change, type and spacing do not.
Theming is the same gesture, made after the import. Redeclare the --margo- variable rather than Tailwind's, so the change reaches the components and your own utilities together, and redeclare it in both blocks — forgetting the .dark one is the usual way a brand accent ends up unreadable at night.
/* 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;
}The mechanism is open to your project as well: declare your own @theme block after the kit and your tokens 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;
}When the change belongs to one place rather than to the system, the same variable can be redeclared on a subtree or inline on the element, and a className merged by cn covers the rest. Which theme the class reflects is your application's decision — the kit never reads prefers-color-scheme on its own.
Component polymorphism
as chooses the element or component to render, and the remaining props are typed against that choice. A Button stays a button by default and becomes a router Link the moment you ask, without the kit knowing anything about routing — to is accepted once it is a Link and rejected when it is not.
<Button as={Link} to="/margo-ui" active>
<Button.Label label="documentation" />
</Button>It matters more than a convenience. The element carries the semantics — what a screen reader announces, what the keyboard can reach, what a middle click does — so rendering the right one is how a component stays correct rather than merely looking correct.