Margo UI
A small React UI kit built on Tailwind CSS v4 design tokens, themeable through plain CSS custom properties.
Installation
npm install margo-uiThe package ships as TypeScript source rather than a build. Your bundler compiles it — which it has to do anyway, since Tailwind scans those same files for the classes to generate. A precompiled bundle would only add a step and hide the source from the scanner.
Getting started
One stylesheet wires everything up. The kit's CSS brings the base layer, the design tokens, the theme bridge, the grid and the utilities; the two font files are separate imports, so a project that already serves its own typography can leave them out.
/* your entry stylesheet */
@import "tailwindcss";
@import "margo-ui/fonts/nunito.css";
@import "margo-ui/fonts/bebas.css";
@import "margo-ui/css";Order matters: tailwindcss first, the kit after, your own overrides last. Everything margo defines is a plain custom property, so the last declaration wins without any specificity war.
Fonts
The kit ships two typefaces, self-hosted as woff2: Nunito for reading, in eight weights, and Bebas Neue for display, in one. They are imported separately from the rest, and that separation is the whole design.
@import "margo-ui/fonts/nunito.css";
@import "margo-ui/fonts/bebas.css";Fonts are the one part of a kit that a project usually already has: a brand typeface, a licence, a provider. Bundling them into margo-ui/css would mean shipping nine font files to every consumer, including the ones that will never render a glyph of them. Keeping them behind their own imports makes them opt-in, one family at a time — you can take Nunito and leave Bebas out.
They are self-hosted rather than fetched from a CDN, which removes a third-party request from the critical path and a privacy question from your legal page. Each face declares font-display: swap, so text paints immediately in the fallback and reflows once the file lands.
The individual files are exported too, which is what makes a preload possible: preload only the weights that appear above the fold, since preloading everything competes with the rest of the page for bandwidth. This site preloads two.
import nunitoExtralightUrl from "margo-ui/fonts/nunito/nunito-extralight.woff2?url";
import nunitoMediumUrl from "margo-ui/fonts/nunito/nunito-medium.woff2?url";
links: [
{ rel: "preload", href: nunitoExtralightUrl, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
{ rel: "preload", href: nunitoMediumUrl, as: "font", type: "font/woff2", crossOrigin: "anonymous" },
];To use your own typefaces, skip the imports entirely and repoint the two variables. Nothing else in the kit names a font: every component reads --margo-font-family-primary or --margo-font-family-secondary, so the substitution reaches all of them at once.
/* your own @font-face declarations, or a provider of your choice */
:root {
--margo-font-family-primary: "Inter", helvetica, sans-serif;
--margo-font-family-secondary: "Anton", impact, sans-serif;
}Keep in mind what the secondary family is used for: it is a display face, set in capitals by design. Swapping in a typeface with lowercase letterforms changes how every title of the kit reads, which is a decision worth making deliberately rather than discovering.
What is in the box
Three things, and they are independent. A set of design tokens — colours, type, elevation — exposed as CSS variables and mirrored into Tailwind's theme. A twelve column grid with named lines and full-bleed support. And the components themselves, which are only the visible consequence of the first two.
You can adopt any one of them alone: the grid works without importing a single component, and the tokens are useful to your own utility classes before they are useful to the kit.
How the components are built
Every component is polymorphic: 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.
<Button as={Link} to="/margo-ui" active>
<Button.Label label="documentation" />
</Button>That machinery is Tag, from react-renderable, which margo uses as its own foundation. Styles are exported as plain class name constants next to each component, and cn — clsx plus tailwind-merge — resolves your className against the defaults, so an override always wins instead of depending on the order the CSS was emitted in.
Composition over configuration
Components that carry more than one slot are compound: Button.Icon, Item.Label, Table.Row. Mounting a part is what makes it exist, so there is no boolean to remember and no prop that silently does nothing — what you see in the JSX is what renders.