design-system Full documentation content. light dark A distributed design-system: nothing to install from npm. It ships as a [shadcn](https://ui.shadcn.com) preset and a shadcn registry, so every pmndrs app — and yours — gets the same colours, fonts and radii by copying code in, not by depending on a package. ![A classroom whiteboard with the word "bleu" written in red and "jaune" in green](bleu-jaune.jpg) shadcn's tokens as the base, Material Design 3 colour roles on top The pmndrs palette baked into the CSS: no provider, no client JavaScript Light and dark schemes, also exported as Figma tokens A git tag as the install address, so every app pins the version it was built against ## Quick start Any app shadcn can init works. With a fresh Next.js one: Create the app ```sh npx create-next-app@latest my-app --ts --tailwind --app --src-dir --yes cd my-app ``` Init shadcn with the poimandres preset ```sh npx shadcn@latest init --preset b1VlIttI ``` Add a pmndrs block Blocks depend on the colour layer, so adding one pulls it in with it: ```sh npx shadcn@latest add pmndrs/docs/keypoints#v4.22.0 ``` Use it ```tsx title="src/app/page.tsx" import { Keypoints, KeypointsItem } from "@/components/keypoints" export default function Home() { return (
The panel sits on bg-surface-dim, an MD3 role shadcn has none for
) } ```
Add the `dark` class to `` for the dark scheme. > [!TIP] > No block in mind? Add the theme on its own — the palette and the mono font: > > ```sh > npx shadcn@latest add pmndrs/design-system/theme#v0.9.0 > ``` ## Tokens Four layers set the tokens, each one deciding what the layer below leaves open: colours · Inter · Inconsolata · --radius"]:3 shadcn["shadcn base-luma
token names · radius scale · element recipe"]:2 mtb["material-theme-builder
MD3 roles"]:1 tailwind["Tailwind v4
spacing · shadows · motion · text-* · utilities"]:3 classDef pmndrsLayer fill:var(--md-sys-color-primary-container),stroke:var(--md-sys-color-primary),color:var(--md-sys-color-on-primary-container) classDef shadcnLayer fill:var(--md-sys-color-inverse-surface),stroke:var(--md-sys-color-outline),color:var(--md-sys-color-inverse-on-surface) classDef mtbLayer fill:var(--md-sys-color-purple-container),stroke:var(--md-sys-color-purple),color:var(--md-sys-color-on-purple-container) classDef tailwindLayer fill:var(--md-sys-color-cyan-container),stroke:var(--md-sys-color-cyan),color:var(--md-sys-color-on-cyan-container) class pmndrs pmndrsLayer class shadcn shadcnLayer class mtb mtbLayer class tailwind tailwindLayer `} /> [shadcn's tokens](https://ui.shadcn.com/docs/theming) are the base — `bg-background`, `text-primary`, `border-border`… keep working as usual. Material Design 3's [colour roles](https://m3.material.io/styles/color/roles) are additive: every `--md-sys-color-*` role becomes a Tailwind colour, for what shadcn has no name for (`bg-surface-dim`, `text-on-surface-variant`, `bg-tertiary-container`…). ## Registry items The design-system is distributed: every `pmndrs/*` repo can contribute blocks, each one installable with a single `shadcn add`. Any public repo becomes a [shadcn registry](https://ui.shadcn.com/docs/registry) with a `registry.json` at its root, and the CLI [installs straight from GitHub](https://ui.shadcn.com/docs/registry/github) — no registry server, no package to publish, just files. A block ships from the repo that already owns it, pmndrs/docs' `Keypoints` for one, so there is nothing to move. This repo holds the theme only: the shared defaults — colours, radius, every shadcn token — that blocks build on, and that your app can still override. Not only UI, either: an item is a set of files, their dependencies and a target path, and the CLI never looks inside. A shader, a drei helper or a `SKILL.md` [ships the same way](https://ui.shadcn.com/docs/registry/github#distribute-anything). An item's address is the registry, the item and a git ref: ```sh npx shadcn@latest add pmndrs/design-system/theme#v0.9.0 ``` The CLI reads `registry.json` and the item's files at that [ref](https://ui.shadcn.com/docs/registry/github#refs) — a tag, a branch or a commit SHA. Pin a tag: an address without one follows the repo's default branch, and changes under you. [`npx shadcn@latest view
`](https://ui.shadcn.com/docs/cli#view) prints an item, files and all, before you [`add`](https://ui.shadcn.com/docs/cli#add) it. ### Namespace This site also serves every item of this repo as static JSON, under `https://pmndrs.github.io/design-system/r/`. Declare it once as a [namespace](https://ui.shadcn.com/docs/registry/namespace) in `components.json`: ```json title="components.json" { "registries": { "@pmndrs": "https://pmndrs.github.io/design-system/r/{name}.json" } } ``` Then an item is a name, no repo and no ref: ```sh npx shadcn@latest add @pmndrs/theme npx shadcn@latest search @pmndrs ``` The namespace serves the latest state of `main`, rebuilt from `registry.json` on every deploy of it. Pin the git address above, at a tag, where an app must not move. A branch's preview deployment serves its own `r/` too, but not all the way down: `preset` depends on `pmndrs/design-system/theme#v0.9.0`, so the theme it installs is the released tag's, not the branch's. The [shadcn MCP server](https://ui.shadcn.com/docs/mcp) reads the same namespace, so Claude, Cursor or VS Code can list, read and install the items from the conversation: ```sh npx shadcn@latest mcp init --client claude ``` A fresh project can start from it too: `preset` is the poimandres preset and the theme in one item, so `init` with its URL does what the Quick start does, without the preset code, and declares the namespace in `components.json` on the way: ```sh npx shadcn@latest init https://pmndrs.github.io/design-system/r/preset.json ``` To prototype in v0 with the pmndrs colours, fonts and radius already applied, [open the theme in v0](https://v0.app/chat/api/open?url=https%3A%2F%2Fpmndrs.github.io%2Fdesign-system%2Fr%2Fv0.json&title=pmndrs). Or have v0 build the brand guidelines from it, as slides: [open the brand guidelines in v0](https://v0.app/chat/api/open?url=https%3A%2F%2Fpmndrs.github.io%2Fdesign-system%2Fr%2Fv0.json&title=pmndrs+brand+guidelines&prompt=pmndrs+brand+guidelines%2C+16%3A9+slides%2C+strict+editorial+grid%2C+layout+inspired+by+https%3A%2F%2Fmir-s3-cdn-cf.behance.net%2Fprojects%2F808%2Fbc1589229031299.Y3JvcCwxNjgzLDEzMTYsMCww.jpg%2C+not+its+colours.+Follow+guidelines%2FGuidelines.md.+app%2Fglobals.css+is+the+palette%3A+never+edit%2C+no+hex%3B+brand+lime+%3D+bg-lime-container+%2B+text-on-lime-container.+Not+the+Poimandres+VS+Code+theme.+Light%2Fdark+toggle.+Cover%3A+%2Fpmndrs%2Flogo_complete.svg.+Then%3A+foreword%2C+logo%2C+colour%2C+type%2C+spacing%2C+radius%2C+icons%2C+components%2C+voice.). v0 reads no namespace, so both open the `v0` item: a v0 project with a starter page, the logo in `public/pmndrs/`, the brand book as `guidelines/Guidelines.md`, and a `globals.css` with the colours resolved, light and dark. Its utilities are the ones a pmndrs project has, shadcn's and the Material Design 3 roles' (`bg-primary-container`, `bg-surface-container-high`, the brand lime as `bg-lime-container`), so what v0 writes runs unchanged in one. ### Publish a block A block has one source of truth, and it moves. It starts in the repo it was born in, used there first — the proof it works, not a component cut off from a real app. It is promoted to pmndrs/design-system only once a second repo reuses it, and promoting it is just moving its files. Any earlier is premature optimisation. So publish it from your own repo: a [`registry.json`](https://ui.shadcn.com/docs/registry/registry-json) at its root, one entry per item in the [`registry-item.json`](https://ui.shadcn.com/docs/registry/registry-item-json) shape. ```json title="registry.json" { "$schema": "https://ui.shadcn.com/schema/registry.json", "name": "pmndrs-", "homepage": "https://github.com/pmndrs/", "items": [ { "name": "", "type": "registry:block", "registryDependencies": [ "sidebar", "collapsible", "pmndrs/design-system/theme#v0.9.0" ], "files": [{ "path": "registry//.tsx", "type": "registry:block" }] } ] } ``` `sidebar` and `collapsible` are stock shadcn components. `pmndrs/design-system/theme` is the pmndrs theme, for a block that paints with an MD3 role; a ref is [not inherited](https://ui.shadcn.com/docs/registry/github#dependency-refs) by dependencies, so it pins its own. [`npx shadcn@latest registry validate`](https://ui.shadcn.com/docs/registry/github#step-3-validate-the-registry) checks the file, and `npx shadcn@latest add pmndrs//#` installs the block anywhere. To list it below, add the repo to [`registry/external.json`](https://github.com/pmndrs/design-system/blob/main/registry/external.json) and run `npm run refresh-catalog`. ### Catalog Here they all are, across repos, each linked to its source, then the same list from [`shadcn search`](https://ui.shadcn.com/docs/cli#search): {/* catalog:start — generated by `npm run build`, edit scripts/catalog.mjs or registry/external.json instead */} | Item | Type | Registry | | --- | --- | --- | | [md3-base](https://github.com/pmndrs/design-system/tree/v0.9.0/registry/md3-base) | lib | [pmndrs/design-system](https://github.com/pmndrs/design-system/blob/v0.9.0/registry.json) | | [font-mono](https://github.com/pmndrs/design-system/tree/v0.9.0/registry/font-mono) | font | [pmndrs/design-system](https://github.com/pmndrs/design-system/blob/v0.9.0/registry.json) | | [theme](https://github.com/pmndrs/design-system/tree/v0.9.0/registry/theme) | lib | [pmndrs/design-system](https://github.com/pmndrs/design-system/blob/v0.9.0/registry.json) | | [preset](https://github.com/pmndrs/design-system/tree/v0.9.0/registry/preset) | base | [pmndrs/design-system](https://github.com/pmndrs/design-system/blob/v0.9.0/registry.json) | | [logo](https://github.com/pmndrs/design-system/tree/v0.9.0/registry/logo) | item | [pmndrs/design-system](https://github.com/pmndrs/design-system/blob/v0.9.0/registry.json) | | [figma-tokens](https://github.com/pmndrs/design-system/tree/v0.9.0/registry/figma-tokens) | item | [pmndrs/design-system](https://github.com/pmndrs/design-system/blob/v0.9.0/registry.json) | | [guidelines](https://github.com/pmndrs/design-system/tree/v0.9.0/registry/guidelines) | item | [pmndrs/design-system](https://github.com/pmndrs/design-system/blob/v0.9.0/registry.json) | | [v0](https://github.com/pmndrs/design-system/tree/v0.9.0/registry/v0) | item | [pmndrs/design-system](https://github.com/pmndrs/design-system/blob/v0.9.0/registry.json) | | [keypoints](https://github.com/pmndrs/docs/tree/v4.22.0/registry/keypoints) | block | [pmndrs/docs](https://github.com/pmndrs/docs/blob/v4.22.0/registry.json) | | [color](https://github.com/pmndrs/docs/tree/v4.22.0/registry/color) | block | [pmndrs/docs](https://github.com/pmndrs/docs/blob/v4.22.0/registry.json) | The CLI lists the same items, read live from each repo at the ref to pin: ```sh npx shadcn@latest search pmndrs/design-system#v0.9.0 pmndrs/docs#v4.22.0 ``` {/* catalog:end */} ]]> ` on `--md-sys-color-`, and `--md-sys-color-on--container` on `--md-sys-color--container`. - Set body text in `foreground` and lower-emphasis text in `muted-foreground`, on `background`, `card`, `popover` or `muted`. - `secondary` and `accent` are the MD3 secondary *container* (`--md-sys-color-secondary-container`), not `--md-sys-color-secondary`. - Draw control boundaries with `input`. `border` is decorative: it is too faint (under 2:1 on `background`) to be the only thing marking a control. - Draw focus with `ring`. - Colour chart series with `chart-1` to `chart-5`, in that order. They are the fixed roles, so a chart keeps its colours in light and dark. - The seed hex itself is the *container* role: `--md-sys-color-primary-container` is the lime. `primary` is a dark olive in light and white in dark, so use the container where the brand colour itself must show. - `--md-sys-color-secondary` and `--md-sys-color-tertiary` are generated but intentionally unused as accents: the named brand colours take their place. - The dark scheme is the `dark` class on ``. - Designers get the same palette as Figma tokens, light and dark as two modes of one variable collection: `npx shadcn@latest add pmndrs/design-system/figma-tokens#v0.9.0`. The same item carries type, spacing, radius and motion as a second collection, and the text and shadow styles as a file for Tokens Studio. ### shadcn tokens and the roles they point at | shadcn token | MD3 role | | --- | --- | | `background` | `--md-sys-color-surface` | | `foreground`, `card-foreground`, `popover-foreground` | `--md-sys-color-on-surface` | | `card`, `sidebar` | `--md-sys-color-surface-container-low` | | `popover` | `--md-sys-color-surface-container-high` | | `muted` | `--md-sys-color-surface-container-highest` | | `muted-foreground` | `--md-sys-color-on-surface-variant` | | `primary`, `ring` | `--md-sys-color-primary` | | `primary-foreground` | `--md-sys-color-on-primary` | | `secondary`, `accent` | `--md-sys-color-secondary-container` | | `secondary-foreground`, `accent-foreground` | `--md-sys-color-on-secondary-container` | | `destructive` | `--md-sys-color-error` | | `input` | `--md-sys-color-outline` | | `border` | `--md-sys-color-outline-variant` | | `chart-1` … `chart-5` | `--md-sys-color-primary-fixed`, `-secondary-fixed`, `-tertiary-fixed`, `-primary-fixed-dim`, `-secondary-fixed-dim` | The `sidebar-*` tokens follow their counterparts: `sidebar-foreground` is `foreground`, `sidebar-primary` is `primary`, `sidebar-primary-foreground` is `primary-foreground`, `sidebar-accent` is `accent`, `sidebar-accent-foreground` is `accent-foreground`, `sidebar-border` is `border`, `sidebar-ring` is `ring`. ### Brand colours The seven brand colours are declared next to the seed as custom colours, none of them blended: each stays its exact hex. Each becomes four roles: `--md-sys-color-`, `-on-`, `--container`, `-on--container`. | Name | Seed | | --- | --- | | `lime` | `#CAF543` | | `teal` | `#00F7A3` | | `cyan` | `#2BDCF6` | | `purple` | `#D855F9` | | `red` | `#FF4980` | | `orange` | `#FFC043` | | `yellow` | `#EBFF0F` | - Lime is also the `source` seed, and red also drives the error role: `destructive` and `--md-sys-color-red` are the same colour. - For the brand hex as a fill, use `--md-sys-color--container` with `--md-sys-color-on--container` on it. - Their shade utilities (`bg-purple-500`) take over Tailwind's stock palettes of the same names. ### Alert colours Five semantic custom colours follow the brand ones, for alerts, hints, badges and statuses. They are seeded with GitHub's alert hues and, unlike the brand colours, blended toward the `source` seed, so they keep their meaning and still sit in the palette. Same four roles each. | Name | Seed | Meaning | | --- | --- | --- | | `note` | `#1F6FEB` | Information the reader should notice | | `tip` | `#238636` | Optional advice that helps | | `important` | `#8957E5` | Information the reader needs to succeed | | `warning` | `#D29922` | Something that needs attention | | `caution` | `#DA3633` | A risk or a negative outcome | - Style an alert with `--md-sys-color--container` and `--md-sys-color-on--container`, and its accent (border, icon, title) with `--md-sys-color-`. - Never use a brand colour for an alert level: `cyan` is not `note`, `red` is not `caution`. ## Type - The preset's font is Inter (`sans`), and headings inherit it. Never hardcode a font family in a block. - The type scale is Tailwind's default: `text-xs` to `text-9xl`, inherited, not yet a pmndrs decision. Set UI text in `text-sm` and body copy in `text-base`; choose weight with Tailwind's `font-*` utilities. - `mono` is Inconsolata, from the `font-mono` registry item that `theme` depends on. It is scoped to `code, kbd, samp, pre`: those elements use it without a class, and the rest of the UI keeps Inter. In app code, add `font-mono` to any other element that should be monospace; a block never does. - Headings, paragraphs, lists and inline code follow shadcn's Typography class recipe (inline code: `font-mono text-sm font-semibold` on `bg-muted`). No element is styled by default. ## Spacing, radii and shadows Poimandres is shadcn-based, so outside colour, fonts and the base radius it embraces Tailwind's defaults. The repository's docs say which is which: the radius is a pmndrs decision; spacing and shadows are inherited from Tailwind v4. - Space with the Tailwind scale, multiples of `--spacing` (0.25rem): step 1 (`p-1`, `gap-1`) is 0.25rem, step 4 (`p-4`) is 1rem; never an arbitrary pixel value. - Every corner derives from one value, `--radius` (0.625rem), from the preset. Its base-luma style multiplies it: `--radius-sm` is ×0.6, `--radius-lg` is `--radius` itself, `--radius-4xl` is ×2.6. Round with `rounded-sm` to `rounded-4xl`; change `--radius` and every corner rescales. `rounded-xs` (`--radius-xs`) is Tailwind's own step, outside that scale. - Elevate with `shadow-2xs` to `shadow-2xl`, recess with `inset-shadow-2xs` to `inset-shadow-sm`, and shadow shapes that are not boxes (icons, SVGs) with `drop-shadow-xs` to `drop-shadow-2xl`. Shadows are black at low opacity in both schemes; in dark, set surfaces apart with the `--md-sys-color-surface-container-*` steps instead. ## Components Poimandres is a distributed architecture based on shadcn blocks: any `pmndrs/*` repository can contribute blocks, each installed with a single `shadcn add` command, plus shared defaults (colours, radius, every shadcn token) that stay overridable by the consumer app. It is fully shadcn-compatible: every shadcn component is supported as is, and takes the palette through the shadcn tokens. The system adds no component library of its own. - Start from a shadcn primitive (`npx shadcn@latest add button`). Never restyle one with hardcoded colours; it already reads `primary`, `border`, `ring` and the rest. - pmndrs' own components are *blocks*, distributed through shadcn's GitHub registries: any public `pmndrs/*` repository with a `registry.json` at its root is a registry. Install one by address: `npx shadcn@latest add pmndrs/docs/keypoints#v4.22.0`. - Always pin a ref (a tag or a sha). Refs are not inherited: every entry in `registryDependencies` carries its own. - A block depends on a shadcn primitive by bare name (`registryDependencies: ["button"]`) and on another pmndrs block by its full pinned address (`pmndrs/docs/mdx-prose#v1.0.0`). A bare name never means a same-repo item. - A block that uses colour depends on `pmndrs/design-system/theme`, so one add pulls the block and the colour layer with it, and the mono font through `font-mono`. `theme` is the single install target: it depends on `md3-base` (the colour machinery and the seed) and `font-mono`. Install `md3-base` alone only to compute a palette of your own. - A block lives in the repository it was born in. It is promoted to `pmndrs/design-system` only once it has shipped in its own repository and a second repository reuses it. There is always exactly one source of truth; promotion moves it. - Blocks are copied code, not a package: the consuming app owns the files and may override any shadcn token. - A registry item is a set of files, so the same mechanism ships more than UI: a shader, a helper, a `SKILL.md`. - Keypoints is the first block, born in `pmndrs/docs`, the generator behind several hundred pages of documentation across the pmndrs libraries. It is released at that repository's `v4.22.0` tag: `npx shadcn@latest add pmndrs/docs/keypoints#v4.22.0`. The same registry also holds Color, swatches of the MD3 roles: `npx shadcn@latest add pmndrs/docs/color#v4.22.0`. Both pin the colour layer, the pmndrs theme, at `pmndrs/design-system/theme#v0.6.0`; in a new app the current one is `pmndrs/design-system/theme#v0.9.0`. Planned, not yet available: shaders from `pmndrs/drei`, assets from `pmndrs/assets`, and blocks from other `pmndrs/*` repositories. Do not assume a block exists; check the owning repository's `registry.json`, or the registry catalog on the docs site, before installing. ## Logo - Use `logo_complete.svg` as the mark, as an image. `logo_idle.svg` is its resting state. - Both paint their own black square background; there is no transparent variant. Never recolour, crop or redraw the mark. - In code, `npx shadcn@latest add pmndrs/design-system/logo#v0.9.0` writes all four states (`logo_complete`, `logo_idle`, `logo_animated`, `logo_loading`) to `public/pmndrs/`. - MIT, like the rest of pmndrs/design-system. No attribution required. ## Iconography - Icons are `lucide`, the pmndrs baseline, taken from the app's `components.json` `iconLibrary`. The repository ships no icon files. ## Voice - British spelling: "colour", "harmonizes". - Short declarative sentences that state a fact and its consequence: "No colour is picked by hand.", "Nothing mounted: the palette is baked into the CSS.", "Always pin a ref". - Lowercase `pmndrs` and `shadcn` in running text; "Poimandres" when naming the collective. No emoji. ]]> No colour is picked by hand. A few seeds go in, and [Material Design 3](https://m3.material.io/styles/color/system/overview) computes every role from them, for light and dark alike. shadcn's tokens are then pointed at those roles. The swatches below are live: toggle the scheme of this site to see the dark palette. ## Seeds Everything on this page derives from these, in [`md3.ts`](https://github.com/pmndrs/design-system/blob/main/registry/md3-base/md3.ts): | Option | Value | | --- | --- | | `source` | `#CAF543` | | `colorMatch` | `true` | | `contrast` | `0` | | `neutral` | `#c1b793` | | `neutralVariant` | `#495720` | | `error` | `#FF4980` | `colorMatch` is Material Theme Builder's "stay true to my color inputs": each seed keeps its chroma, and lands in its container role. It takes the place of a `scheme`. The two neutral seeds look nothing like the grey ramps they produce, by design: under `colorMatch` a neutral ramp takes an eighth of its seed's chroma, so a seed carries eight times what comes back. The posters below follow the same seed: this site's theme is built from it, `colorMatch`, the neutrals and `error` included, so what they render is the palette `md3.ts` ships. ## Poster Material's [scheme poster](https://m3.material.io/styles/color/roles) of the palette: each role, and on it the role meant to go on it. Every one is a Tailwind colour, `bg-` / `text-`.
{['primary', 'secondary', 'tertiary'].map((role) => ( ))} {['primary', 'secondary', 'tertiary'].map((role) => ( ))}
## shadcn tokens shadcn's tokens keep their names: `bg-background`, `text-primary`, `border-border`… all work as usual. Each one is pointed at an MD3 role, the same poster with shadcn's names on it:
primary · ring primary-foreground secondary · accent secondary-foreground · accent-foreground destructive background card · sidebar popover muted foreground muted-foreground input border
chart-1 chart-2 chart-3 chart-4 chart-5
The `sidebar-*` tokens follow their counterparts: `sidebar-primary` is `primary`, `sidebar-accent` is `accent`, `sidebar-border` is `border`, `sidebar-ring` is `ring`. Everything shadcn has no name for is there too, under its MD3 name: `bg-surface-dim`, `bg-tertiary-container`, `text-on-surface-variant`… ## Custom colours Colours Material has no role for — a brand palette, status levels — are declared next to the seeds, as `customColors`. Each one becomes four roles, ``, `on-`, `-container` and `on--container`, computed for light and dark like the others, plus eleven shades, `-50` … `-950`. `blend` harmonizes it toward the `source` seed; without it the colour stays true to its hex. For custom colours of your *own*, do not edit the installed `md3.ts`: spread `pmndrsMtb` into your config, add yours there, and name them in the `@plugin` body — `md3-base`'s docs walk through it. Twelve are declared this way, in this order: the seven brand colours, then the five alert colours. The brand colours are not blended, the alert colours are. ### Brand colours The seven brand colours are none of them blended: each stays its exact hex. Lime is also the `source` seed, and red also drives the `error` role. Their shade utilities take over Tailwind's stock palettes of the same names. | Name | Seed | Blend | Utilities | | --- | --- | --- | --- | | `lime` | `#CAF543` | no | `bg-lime`, `text-on-lime`, `bg-lime-container`, `text-on-lime-container` | | `teal` | `#00F7A3` | no | `bg-teal`, `text-on-teal`, `bg-teal-container`, `text-on-teal-container` | | `cyan` | `#2BDCF6` | no | `bg-cyan`, `text-on-cyan`, `bg-cyan-container`, `text-on-cyan-container` | | `purple` | `#D855F9` | no | `bg-purple`, `text-on-purple`, `bg-purple-container`, `text-on-purple-container` | | `red` | `#FF4980` | no | `bg-red`, `text-on-red`, `bg-red-container`, `text-on-red-container` | | `orange` | `#FFC043` | no | `bg-orange`, `text-on-orange`, `bg-orange-container`, `text-on-orange-container` | | `yellow` | `#EBFF0F` | no | `bg-yellow`, `text-on-yellow`, `bg-yellow-container`, `text-on-yellow-container` | {['lime', 'teal', 'cyan', 'purple', 'red', 'orange', 'yellow'].map((name) => ( ))} ### Alert colours The five alert colours are the design system's semantic roles. Use them for GitHub alerts (`> [!NOTE]`…), hints, badges and statuses. They are seeded with GitHub's alert hues, so a reader recognises them at a glance. They are blended: each is pulled toward the `source` seed. So it keeps its meaning and still sits in the palette, this one or that of any library that reseeds it. A brand colour would not do. `cyan` is not a note, and unblended, its container is far too loud for an alert's background. | Name | Seed | Blend | Override | Utilities | | --- | --- | --- | --- | --- | | `note` | `#1F6FEB` | yes | `THEME_NOTE` | `bg-note`, `text-on-note`, `bg-note-container`, `text-on-note-container` | | `tip` | `#238636` | yes | `THEME_TIP` | `bg-tip`, `text-on-tip`, `bg-tip-container`, `text-on-tip-container` | | `important` | `#8957E5` | yes | `THEME_IMPORTANT` | `bg-important`, `text-on-important`, `bg-important-container`, `text-on-important-container` | | `warning` | `#D29922` | yes | `THEME_WARNING` | `bg-warning`, `text-on-warning`, `bg-warning-container`, `text-on-warning-container` | | `caution` | `#DA3633` | yes | `THEME_CAUTION` | `bg-caution`, `text-on-caution`, `bg-caution-container`, `text-on-caution-container` | {['note', 'tip', 'important', 'warning', 'caution'].map((name) => ( ))} Each seed can be overridden from the environment, as the primary can with `THEME_PRIMARY`; the baked `theme` is always built without them, so it ships the seeds above. To change any custom colour, edit `customColors` in [`md3.ts`](https://github.com/pmndrs/design-system/blob/main/registry/md3-base/md3.ts), run `npm run build`, and update these tables. The swatches above are this site's own theme, which the docs engine computes with the same custom colours. ## Tonal palettes Under the roles sit Material's tonal palettes: one ramp per seed — `primary`, `secondary`, `tertiary`, `neutral`, `neutral-variant`, `error` — and one per custom colour, each taken at the same 28 tones, from 100, white, down to 0, black. A role is an alias onto one of these tones: `surface` is `neutral-98` in light and `neutral-6` in dark, `lime` is `lime-40` and `lime-100`. The one exception is a container under `colorMatch`, in light: it is its seed's own hex, which no tone of the ramp matches exactly. The tones themselves have no scheme: the swatches below are the same in light and dark. Each one is a CSS variable, `--md-ref-palette--`, with no Tailwind utility of its own — reach one with an arbitrary value, `bg-(--md-ref-palette-neutral-variant-60)`. Prefer a role where one fits: a role follows the scheme, a tone does not.
{[100, 99, 98, 96, 95, 94, 92, 90, 87, 80, 70, 60, 50, 40, 35, 30, 25, 24, 22, 20, 17, 15, 12, 10, 6, 5, 4, 0].map((tone) => ( {tone} ))}
{['primary', 'secondary', 'tertiary', 'neutral', 'neutral-variant', 'error', 'lime', 'teal', 'cyan', 'purple', 'red', 'orange', 'yellow', 'note', 'tip', 'important', 'warning', 'caution'].map((palette) => (
{palette} {[100, 99, 98, 96, 95, 94, 92, 90, 87, 80, 70, 60, 50, 40, 35, 30, 25, 24, 22, 20, 17, 15, 12, 10, 6, 5, 4, 0].map((tone) => ( ))}
))}
## Figma tokens The same palette, for designers: light and dark as two modes of one Figma variable collection, in [DTCG](https://www.designtokens.org) files. Every role is the hex the CSS gives it, alias for alias. Install them next to your code, into `design/tokens/pmndrs/` at the root of your project: ```sh npx shadcn@latest add pmndrs/design-system/figma-tokens#v0.9.0 ``` Or download them: [light](https://github.com/pmndrs/design-system/blob/main/figma/Light.tokens.json), [dark](https://github.com/pmndrs/design-system/blob/main/figma/Dark.tokens.json). The same item carries the other foundations: type, spacing, radius and motion as a second variable collection, [`Foundations.tokens.json`](https://github.com/pmndrs/design-system/blob/main/figma/Foundations.tokens.json), and the text and shadow styles as a file for Tokens Studio, [`Styles.tokens.json`](https://github.com/pmndrs/design-system/blob/main/figma/Styles.tokens.json). The item's [docs](https://github.com/pmndrs/design-system/blob/main/registry/figma-tokens/docs.md) walk through both imports. ## Reseeding The palette is computed, never written by hand: change the seeds in [`md3.ts`](https://github.com/pmndrs/design-system/blob/main/registry/md3-base/md3.ts) and run `npm run build`; the baked CSS of the registry and the [Figma tokens](https://github.com/pmndrs/design-system/tree/main/figma) follow. For a palette of your own, install `md3-base` instead of `theme` and emit the roles from your seeds: ```sh npx shadcn@latest add pmndrs/design-system/md3-base#v0.9.0 ``` ]]> Two font families: Inter for everything, Inconsolata for code. Sizes come from Tailwind's `text-*` scale, and headings, paragraphs and lists follow shadcn's class recipe. ## Font family | Role | Family | Utility | Variable | Comes from | | ---- | ----------- | ----------- | ------------- | --------------------------------------------- | | Sans | Inter | `font-sans` | `--font-sans` | the preset `b1VlIttI` | | Mono | Inconsolata | `font-mono` | `--font-mono` | the `font-mono` registry item, via `theme` | **Inter** comes from the poimandres shadcn preset (`b1VlIttI`, font `inter`), applied by `npx shadcn@latest init --preset b1VlIttI`. On Next.js it loads through `next/font/google` as `--font-sans`. Headings use the same family: the preset sets `--font-heading: var(--font-sans)`. **Inconsolata** comes from the `font-mono` registry item, which `theme` depends on, so adding the theme installs it: ```sh npx shadcn@latest add pmndrs/design-system/theme#v0.9.0 ``` It is scoped to `code, kbd, samp, pre`: those elements use it without a class, and the rest of the UI keeps Inter. In your own app code, add the `font-mono` utility to any other element that should be monospace. Blocks never do: they inherit the font of the app they land in. The specimens below render with this site's CSS, which may not load the same fonts as your project. The table is the source of truth.

Aa Bb Cc 0123

The quick brown fox jumps over the lazy dog.

font-sans

Aa Bb Cc 0123

The quick brown fox jumps over the lazy dog.

font-mono
## Type scale > [!NOTE] > Inherited from Tailwind v4 defaults, not yet a pmndrs decision. See [Tailwind's font-size docs](https://tailwindcss.com/docs/font-size). Each `text-*` utility sets a font size and its paired line height, from the `--text-*` and `--text-*--line-height` theme variables. Pixels assume the browser's default 16px root. The previews render with this site's CSS: its root is 17px, so they draw slightly larger than in your project. | Utility | Font size | Line height | Preview | | ----------- | --------------------- | ------------------------------- | -------------------------------- | | `text-xs` | `0.75rem` (12px) | `calc(1 / 0.75)` (16px) | Aa | | `text-sm` | `0.875rem` (14px) | `calc(1.25 / 0.875)` (20px) | Aa | | `text-base` | `1rem` (16px) | `calc(1.5 / 1)` (24px) | Aa | | `text-lg` | `1.125rem` (18px) | `calc(1.75 / 1.125)` (28px) | Aa | | `text-xl` | `1.25rem` (20px) | `calc(1.75 / 1.25)` (28px) | Aa | | `text-2xl` | `1.5rem` (24px) | `calc(2 / 1.5)` (32px) | Aa | | `text-3xl` | `1.875rem` (30px) | `calc(2.25 / 1.875)` (36px) | Aa | | `text-4xl` | `2.25rem` (36px) | `calc(2.5 / 2.25)` (40px) | Aa | | `text-5xl` | `3rem` (48px) | `1` (48px) | Aa | | `text-6xl` | `3.75rem` (60px) | `1` (60px) | Aa | | `text-7xl` | `4.5rem` (72px) | `1` (72px) | Aa | | `text-8xl` | `6rem` (96px) | `1` (96px) | Aa | | `text-9xl` | `8rem` (128px) | `1` (128px) | Aa | Override the line height with a slash: `text-sm/6` is `0.875rem` on a `1.5rem` line. ## Font weight > [!NOTE] > Inherited from Tailwind v4 defaults, not yet a pmndrs decision. See [Tailwind's font-weight docs](https://tailwindcss.com/docs/font-weight). `font-*` sets `font-weight` from the `--font-weight-*` theme variable. Text is `font-normal` unless a class says otherwise. Inter covers every step; Inconsolata starts at 200, so `font-thin` on code renders at 200. | Utility | Variable | Value | Preview | | ----------------- | -------------------------- | ----- | --------------------------------------------- | | `font-thin` | `--font-weight-thin` | `100` | Aa | | `font-extralight` | `--font-weight-extralight` | `200` | Aa | | `font-light` | `--font-weight-light` | `300` | Aa | | `font-normal` | `--font-weight-normal` | `400` | Aa | | `font-medium` | `--font-weight-medium` | `500` | Aa | | `font-semibold` | `--font-weight-semibold` | `600` | Aa | | `font-bold` | `--font-weight-bold` | `700` | Aa | | `font-extrabold` | `--font-weight-extrabold` | `800` | Aa | | `font-black` | `--font-weight-black` | `900` | Aa | ## Elements > [!NOTE] > Inherited from shadcn, not yet a pmndrs decision. See [shadcn's Typography page](https://ui.shadcn.com/docs/components/typography). No element is styled by default: this is a recipe of utility classes to put on the elements yourself. Each preview below is followed by its classes. ### h1

Ready-made building blocks

```tsx

``` ### h2

Getting started

```tsx

``` ### h3

Install the theme

```tsx

``` ### h4

Add the dark class

```tsx

``` ### p

Every pmndrs project gets the same colours, fonts and radii by copying code in, not by depending on a package.

```tsx

``` ### Lead

A design system shipped as a shadcn registry.

```tsx

``` ### Large

Ready to install?
```tsx
``` ### Small
Registry item
```tsx ``` ### Muted

Add the dark class to html for the dark scheme.

```tsx

``` ### Blockquote

Whatever the preset or the registry fixes is a pmndrs decision.
```tsx
``` ### List
  • Colours from the theme
  • Inter from the preset
  • Inconsolata from the registry
```tsx
    ``` ### Inline code
    npx shadcn@latest add
    ```tsx ``` ]]> Every spacing utility is a multiple of one base unit, `--spacing`. A utility `n` is `n × 0.25rem`: `p-4` is `1rem`, `gap-2` is `0.5rem`. > [!NOTE] > Inherited from Tailwind v4 defaults, not yet a pmndrs decision. See [Tailwind's padding docs](https://tailwindcss.com/docs/padding). ## Base unit | Token | Value | | ----------- | --------- | | `--spacing` | `0.25rem` | The same scale drives padding (`p-*`), margin (`m-*`), gaps (`gap-*`, `space-*`), sizes (`w-*`, `h-*`, `size-*`, `min-*`, `max-*`), offsets (`inset-*`, `top-*`…) and translations (`translate-*`). Any multiple works, not only the steps below: `p-13` is `3.25rem`. Prefix a utility with `-` for a negative value (`-mt-2`), and use brackets for a one-off (`p-[5px]`). ## Scale The common steps, with the value your project gets. Pixels assume the browser's default 16px root. The bars are live `w-*` utilities, rendered with this site's CSS: its root is 17px, so they draw slightly wider than in your project. The table is the source of truth. | Utility | Value | Pixels | Preview | | ------- | ---------- | ------ | ---------------------------------------- | | `0` | `0` | 0 |
    | | `px` | `1px` | 1px |
    | | `0.5` | `0.125rem` | 2px |
    | | `1` | `0.25rem` | 4px |
    | | `1.5` | `0.375rem` | 6px |
    | | `2` | `0.5rem` | 8px |
    | | `2.5` | `0.625rem` | 10px |
    | | `3` | `0.75rem` | 12px |
    | | `3.5` | `0.875rem` | 14px |
    | | `4` | `1rem` | 16px |
    | | `5` | `1.25rem` | 20px |
    | | `6` | `1.5rem` | 24px |
    | | `7` | `1.75rem` | 28px |
    | | `8` | `2rem` | 32px |
    | | `9` | `2.25rem` | 36px |
    | | `10` | `2.5rem` | 40px |
    | | `11` | `2.75rem` | 44px |
    | | `12` | `3rem` | 48px |
    | | `14` | `3.5rem` | 56px |
    | | `16` | `4rem` | 64px |
    | | `20` | `5rem` | 80px |
    | | `24` | `6rem` | 96px |
    | | `28` | `7rem` | 112px |
    | | `32` | `8rem` | 128px |
    | | `36` | `9rem` | 144px |
    | | `40` | `10rem` | 160px |
    | | `44` | `11rem` | 176px |
    | | `48` | `12rem` | 192px |
    | | `52` | `13rem` | 208px |
    | | `56` | `14rem` | 224px |
    | | `60` | `15rem` | 240px |
    | | `64` | `16rem` | 256px |
    | | `72` | `18rem` | 288px |
    | | `80` | `20rem` | 320px |
    | | `96` | `24rem` | 384px |
    | ]]> Every corner in a pmndrs project derives from one value, `--radius`. The `rounded-*` utilities are multiples of it, so changing that one value rescales every corner at once. ## Base ```css :root { --radius: 0.625rem; /* 10px at a 16px root */ } ``` It comes from the poimandres shadcn preset (`b1VlIttI`, style `base-luma`, radius `default`), applied by `npx shadcn@latest init --preset b1VlIttI`. ## Scale The preset's `base-luma` style derives the scale by multiplication: each step is `--radius` times a factor. Values below are at a 16px root. | Token | Utility | Value | rem | px | | -------------- | -------------- | ----------------------------- | ---------- | ---- | | `--radius-xs` | `rounded-xs` | inherited from Tailwind v4 | `0.125rem` | 2px | | `--radius-sm` | `rounded-sm` | `calc(var(--radius) * 0.6)` | `0.375rem` | 6px | | `--radius-md` | `rounded-md` | `calc(var(--radius) * 0.8)` | `0.5rem` | 8px | | `--radius-lg` | `rounded-lg` | `var(--radius)` | `0.625rem` | 10px | | `--radius-xl` | `rounded-xl` | `calc(var(--radius) * 1.4)` | `0.875rem` | 14px | | `--radius-2xl` | `rounded-2xl` | `calc(var(--radius) * 1.8)` | `1.125rem` | 18px | | `--radius-3xl` | `rounded-3xl` | `calc(var(--radius) * 2.2)` | `1.375rem` | 22px | | `--radius-4xl` | `rounded-4xl` | `calc(var(--radius) * 2.6)` | `1.625rem` | 26px | The table is the source of truth: the previews below render this site's own radius scale, which can differ slightly from what your project gets. ## Preview
    rounded-sm
    rounded-md
    rounded-lg
    rounded-xl
    rounded-2xl
    rounded-3xl
    rounded-4xl
    ]]> Three families of shadow, each a scale of Tailwind utilities: `shadow-*` for elevation, `inset-shadow-*` for recessed surfaces, and `drop-shadow-*` for shapes that are not boxes. > [!NOTE] > Inherited from Tailwind v4 defaults, not yet a pmndrs decision. See [Tailwind's box-shadow docs](https://tailwindcss.com/docs/box-shadow). Every shadow is black at low opacity. On a dark surface, the lighter ones barely show. Each family also has a `-none` utility that removes the shadow. ## Box shadow `shadow-*` sets `box-shadow` from the `--shadow-*` theme variable: | Utility | Variable | Value | | ------------ | -------------- | --------------------------------------------------------------- | | `shadow-2xs` | `--shadow-2xs` | `0 1px rgb(0 0 0 / 0.05)` | | `shadow-xs` | `--shadow-xs` | `0 1px 2px 0 rgb(0 0 0 / 0.05)` | | `shadow-sm` | `--shadow-sm` | `0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)` | | `shadow-md` | `--shadow-md` | `0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)` | | `shadow-lg` | `--shadow-lg` | `0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)` | | `shadow-xl` | `--shadow-xl` | `0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)` | | `shadow-2xl` | `--shadow-2xl` | `0 25px 50px -12px rgb(0 0 0 / 0.25)` |
    {['shadow-2xs', 'shadow-xs', 'shadow-sm', 'shadow-md', 'shadow-lg', 'shadow-xl', 'shadow-2xl'].map((shadow) => (
    {shadow}
    ))}
    ## Inset shadow `inset-shadow-*` sets an inner `box-shadow` from the `--inset-shadow-*` theme variable. It stacks with `shadow-*` on the same element: | Utility | Variable | Value | | ------------------ | -------------------- | ----------------------------------- | | `inset-shadow-2xs` | `--inset-shadow-2xs` | `inset 0 1px rgb(0 0 0 / 0.05)` | | `inset-shadow-xs` | `--inset-shadow-xs` | `inset 0 1px 1px rgb(0 0 0 / 0.05)` | | `inset-shadow-sm` | `--inset-shadow-sm` | `inset 0 2px 4px rgb(0 0 0 / 0.05)` |
    {['inset-shadow-2xs', 'inset-shadow-xs', 'inset-shadow-sm'].map((shadow) => (
    {shadow}
    ))}
    ## Drop shadow `drop-shadow-*` applies `filter: drop-shadow()` from the `--drop-shadow-*` theme variable. It follows the painted shape, transparent pixels excluded, so it suits icons, SVGs and cut-out images: | Utility | Variable | Value | | ----------------- | ------------------- | ------------------------------ | | `drop-shadow-xs` | `--drop-shadow-xs` | `0 1px 1px rgb(0 0 0 / 0.05)` | | `drop-shadow-sm` | `--drop-shadow-sm` | `0 1px 2px rgb(0 0 0 / 0.15)` | | `drop-shadow-md` | `--drop-shadow-md` | `0 3px 3px rgb(0 0 0 / 0.12)` | | `drop-shadow-lg` | `--drop-shadow-lg` | `0 4px 4px rgb(0 0 0 / 0.15)` | | `drop-shadow-xl` | `--drop-shadow-xl` | `0 9px 7px rgb(0 0 0 / 0.1)` | | `drop-shadow-2xl` | `--drop-shadow-2xl` | `0 25px 25px rgb(0 0 0 / 0.15)` |
    {['drop-shadow-xs', 'drop-shadow-sm', 'drop-shadow-md', 'drop-shadow-lg', 'drop-shadow-xl', 'drop-shadow-2xl'].map((shadow) => (
    {shadow}
    ))}
    ]]>
    Two families of motion utilities: `duration-*` sets how long a transition runs, and `ease-*` the curve it follows. A transition that names neither runs on one default for each. > [!NOTE] > Inherited from Tailwind v4 defaults, not yet a pmndrs decision. See [Tailwind's transition-duration docs](https://tailwindcss.com/docs/transition-duration). ## Defaults A `transition-*` utility (`transition`, `transition-colors`, `transition-transform`…) without a `duration-*` or an `ease-*` runs on these two theme variables: | Variable | Value | | -------------------------------------- | ------------------------------ | | `--default-transition-duration` | `150ms` | | `--default-transition-timing-function` | `cubic-bezier(0.4, 0, 0.2, 1)` | The default curve is the one `ease-in-out` sets. ## Duration `duration-*` sets `transition-duration` in milliseconds: `duration-300` is `300ms`. No theme variable stands behind it, so any number works, not only the steps below: `duration-250` is `250ms`. Use brackets for another unit (`duration-[2s]`). | Utility | Value | | --------------- | -------- | | `duration-0` | `0ms` | | `duration-75` | `75ms` | | `duration-100` | `100ms` | | `duration-150` | `150ms` | | `duration-200` | `200ms` | | `duration-300` | `300ms` | | `duration-500` | `500ms` | | `duration-700` | `700ms` | | `duration-1000` | `1000ms` | Hover the panel to run every step at once, on the default curve:
    {['duration-0', 'duration-75', 'duration-100', 'duration-150', 'duration-200', 'duration-300', 'duration-500', 'duration-700', 'duration-1000'].map((duration) => (
    {duration}
    ))}
    ## Easing `ease-*` sets `transition-timing-function`, from the `--ease-*` theme variables. See [Tailwind's transition-timing-function docs](https://tailwindcss.com/docs/transition-timing-function). | Utility | Variable | Value | | ------------- | --------------- | ------------------------------ | | `ease-linear` | none | `linear` | | `ease-in` | `--ease-in` | `cubic-bezier(0.4, 0, 1, 1)` | | `ease-out` | `--ease-out` | `cubic-bezier(0, 0, 0.2, 1)` | | `ease-in-out` | `--ease-in-out` | `cubic-bezier(0.4, 0, 0.2, 1)` | `ease-in` starts slow, so it suits an element leaving the screen. `ease-out` ends slow, so it suits an element arriving. `ease-in-out` suits an element that moves from one place to another. Hover the panel to run every curve at once, over `duration-1000`:
    {['ease-linear', 'ease-in', 'ease-out', 'ease-in-out'].map((ease) => (
    {ease}
    ))}
    ## Reduced motion Prefix a utility with `motion-reduce:` to drop a transition for people who ask their system for less motion: `motion-reduce:transition-none`. The previews above do. ]]> The pmndrs assets, each one installed into your app from the registry with one command. The logo is the first. ## Logo The pmndrs logo, in four states, as four SVG files. Install them with one command: ```sh npx shadcn@latest add pmndrs/design-system/logo#v0.9.0 ``` That writes them to `public/pmndrs/` at the root of your project, under the names below. Every file is 600×600 and paints its own black square background: there is no transparent variant yet. ### Variants | Variant | Preview | File | Behavior | | -------- | ------------------------------------------------------------------------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------- | | Complete | The pmndrs logo | `public/pmndrs/logo_complete.svg` | The full mark. | | Idle | The pmndrs logo at rest, four concentric rings | `public/pmndrs/logo_idle.svg` | The resting state: four concentric rings. | | Animated | The pmndrs logo, springing from idle to complete | `public/pmndrs/logo_animated.svg` | Idle → complete, once, in 0.73 s. Under reduced motion, the complete mark shows without moving. | | Loading | The pmndrs logo, looping as a loader | `public/pmndrs/logo_loading.svg` | Loops idle → each corner → idle, every 5.3 s. Under reduced motion, the mark only fades. | ### Usage Once installed (or copied into your app's `public/pmndrs/` by hand), each file is an image URL. Both animations are CSS inside the SVG, so a plain `` plays them, and so does `next/image` with `unoptimized`: ```tsx Loading ``` ## License These assets are MIT-licensed, like the rest of the repository: see [LICENSE](https://github.com/pmndrs/design-system/blob/main/LICENSE). No attribution required. ]]>