@protolabsai/design
v0.8.0
Published
protoLabs.studio brand design system — canonical tokens, CSS custom properties, a Tailwind preset, and brand assets. One source, every site.
Readme
@protolabsai/design
The protoLabs.studio brand, as code. One canonical token set → CSS custom properties, a Tailwind preset, and the brand assets. Every studio site consumes this so the brand can't drift across surfaces.
Source of truth for the values; docs/reference/visual-identity.md is the source of truth for the rules (when to use what, what we don't do).
Install
pnpm add @protolabsai/design(Workspace-internal today; published to GitHub Packages when a non-monorepo site needs it.)
Use it
Plain CSS (the marketing site, any framework) — import the bundle once at the root. It sets the --pl-* custom properties and base element styles:
@import "@protolabsai/design/css";Then reference tokens anywhere:
.card {
background: var(--pl-color-bg-raised);
border: var(--pl-border-width) solid var(--pl-color-border);
border-radius: var(--pl-radius);
}Tailwind — add the preset; utilities like bg-bg, text-fg-muted, rounded, font-mono, shadow-glow, bg-brand-gradient resolve to the locked values:
// tailwind.config.js
module.exports = { presets: [require("@protolabsai/design/tailwind")] };Tokens in JS/TS — for anywhere you need the raw values (OG images, canvas, emails):
import { tokens } from "@protolabsai/design/tokens";
tokens.color.brand.lavender; // "#9b87f2"Assets — logos, icon treatments, banner, favicon:
@protolabsai/design/assets/protolabs-icon-outline.svg
@protolabsai/design/assets/readme-banner.pngassets/ is the one source of truth for the brand marks, same as src/tokens.js
is for the values. A browser can't load a favicon from node_modules, so each
surface needs the mark in its own served dir — but those copies are generated, not
committed. This package ships a bin, protolabs-sync-assets, that copies from
assets/ into the served dirs a repo declares; the copies are gitignored, so there's
nothing to drift. Recolor/replace a mark here, re-sync, and every surface follows.
The fleet pattern — any repo consuming this package:
- Add a
brand-assets.config.jsonat the repo root mapping surfaces → dest dirs:
A map of{ "surfaces": { "docs": { "docs/public": { "protolabs-icon-outline.svg": "protolabs-icon-outline.svg" } }, "gallery": { "docs/assets/brand": "*" } } }"*"mirrors every asset (for a GitHub-rendered gallery, kept committed). - Prefix builds with the sync:
"docs:build": "protolabs-sync-assets docs && vitepress build docs". - Gitignore the generated served copies; for a committed mirror, guard it in CI with
protolabs-sync-assets gallery && git diff --exit-code -- <dir>.
The bin finds the config by walking up from cwd, so it works from the repo root or a
nested app's prebuild. See this repo's brand-assets.config.json for a live example.
Fonts — Geist + Geist Mono (self-host or):
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet" />What's in the box
| Path | What |
|---|---|
| src/tokens.js | The source of truth. Edit here. |
| dist/tokens.css | Generated --pl-* custom properties. |
| dist/tokens.json | Generated token tree (preset + tooling read this). |
| css/base.css | Reset + element defaults built on the tokens. |
| css/index.css | The bundle (tokens.css + base.css). |
| tailwind/preset.cjs | Tailwind theme mapping. |
| assets/ | Logos, icon treatments, banner. The favicon/in-app mark is protolabs-icon-outline.svg (transparent ground). |
Changing a token
Edit src/tokens.js, then regenerate — never hand-edit dist/:
pnpm --filter @protolabsai/design buildThe generated CSS + JSON are committed so consumers don't need a build step. Keeping dist/ generated (not authored) is the anti-drift guarantee: the CSS the sites use and the values the preset reads come from the same object.
Locked values (2026-05-29)
Where the spec (visual-identity.md) and the shipped site (styles.css) disagreed, the shipped site won: 4px radius, #0a0a0c ground, #ededed foreground, bordered (not filled) primary buttons. Status colors are low-chroma OKLCH. Dark-first; the gradient swaps to a saturated variant under prefers-color-scheme: light. Full rationale in visual-identity.md and foundation.md §13.
When in doubt: make it grayer and smaller.
