Guides
Migration notes, agent skills, and contributor workflows for @tollerud/ui.
Email (@tollerud/email)
HTML email is a separate render target — table layout, inline styles, no CSS variables. It ships as its own package that shares Tollerud's tokens (inlined as literals), not the web components. Built on React Email. Never render @tollerud/ui components into email.
Primitives: EmailLayout, EmailHeader (monogram + project name large), BrandMark, EmailButton, EmailHeading, EmailText, EmailDivider, EmailFooter (Tollerud footer → tollerud.no; wordmark recolors as one in dark clients, centered fine print below). Templates: WelcomeEmail, VerifyEmail, PasswordResetEmail, ReceiptEmail — each takes optional header and an overridable copy (reword / i18n). Every primitive takes an optional style escape hatch. Render with the re-exported render.
import { render, WelcomeEmail, EmailLayout, EmailHeader, EmailButton, EmailText, EmailFooter } from '@tollerud/email'
// Ready-made template — opt into the branded header (monogram + project name)
const html = await render(
<WelcomeEmail
name="Mathias"
productName="Graphify"
ctaUrl={dashboardUrl}
header={{ productName: 'Graphify' }}
footer={{ labels: { tollerudProject: 'A Tollerud Project' }, unsubscribeUrl }}
/>,
)
// Or compose your own from primitives
const custom = await render(
<EmailLayout preview="Your report is ready">
<EmailHeader productName="Graphify" />
<EmailText>Your weekly report is ready to view.</EmailText>
<EmailButton href={reportUrl}>View report</EmailButton>
<EmailFooter unsubscribeUrl={unsubscribeUrl} />
</EmailLayout>,
)
// hand the HTML to your mailer (Resend, SES, Nodemailer, …)Migration
Breaking changes ship with CHANGELOG entries. Recent majors to know about.
Replace require('@tollerud/ui') with ESM imports. CJS subpaths are removed.
Install Radix, Lucide, Framer Motion, and Sonner explicitly in consumer apps.
Projects that vendored src/components/ui/* should delete copies and import from the package. See AGENTS.md in the repo.
Tailwind remains available, but consumer apps should use exported components first and reserve utilities for small layout glue.
Consumer project checklist
/resources/consumer-checklist/Self-audit styling setup before shipping. Catches copied components, missing source.css, and invalid Button/Link composition.
# From your consumer app root
npx tollerud-ui-audit
# Monorepo app package
npx tollerud-ui-audit ./apps/web
# Without npx (direct script path)
node node_modules/@tollerud/ui/scripts/audit-consumer-styling.mjs
# Advisory CI — print findings but exit 0 even with errors
npx tollerud-ui-audit --warn-onlyWhat the audit checks
Missing @tollerud/ui in package.json, missing globals.css or source.css imports, local components/ui clones, tollerud-* classes without package imports, hardcoded brand hex values, local cn() helpers, nested Button/Link patterns, and components/ui re-export shims.
Exit codes
Exit 0 when no issues are found, or when --warn-only is set. Exit 1 when errors are found — use in required CI checks.
Audit error codes
Findings print as ERROR [code] or WARN [code]:
| Code | Level | Fix |
|---|---|---|
| missing-ui-dep | error | Add @tollerud/ui and peers to package.json |
| missing-globals-css | error | Import @tollerud/ui/globals.css in Tailwind entry |
| missing-source-css | error | Import @tollerud/ui/source.css (prevents production purge) |
| local-ui-clone | error | Delete components/ui copies; import from @tollerud/ui |
| copied-ds-tokens | error | Replace vendored files using tollerud-* without package imports |
| hardcoded-hex | error | Use text-tollerud-yellow, bg-tollerud-noir-950 tokens |
| button-link-nesting | error | Button asChild + Link, or buttonVariants() on the link |
| ui-reexport-shim | warn | Import from @tollerud/ui instead of local ui/index re-exports |
| local-cn | warn | import { cn } from "@tollerud/ui/utils" |
| generic-yellow-util | warn | Prefer Button variant="primary" or text-tollerud-yellow |
| no-globals-css | warn | Verify Tailwind entry imports both Tollerud CSS files |
| no-package-json | warn | Run audit from the consumer app package root |
// src/features/hosts/HostDeployPanel.tsx — app-specific; composes @tollerud/ui
import { Button, FormPanel, Input, Stack } from '@tollerud/ui'
export function HostDeployPanel({ onDeploy }: { onDeploy: (host: string) => void }) {
return (
<FormPanel
title="Connect host"
footer={<Button variant="primary" onClick={() => onDeploy('emma.tollerud.no')}>Connect</Button>}
>
<Stack gap="md">
<Input label="Hostname" placeholder="emma.tollerud.no" />
</Stack>
</FormPanel>
)
}AI agents
Sync SKILL.md into your project so coding assistants use verified exports and gotchas.
# Claude Code / Cursor — copy verbatim on version bumps
mkdir -p .claude/skills/tollerud-ui
curl -fsSL https://raw.githubusercontent.com/Tollerud/ui/main/SKILL.md \
-o .claude/skills/tollerud-ui/SKILL.md
# Current package version: v5.6.0SKILL.md reflects actual components/index.ts exports — more reliable than stale copy-paste snippets.
Contributing
Validate before opening a PR. Component changes need registry + docs demo updates.
npm run validate # typecheck, lint, test, build, package gates, docs build
# Component checklist (see AGENTS.md):
# components/*.tsx → index.ts → registry.json → docs page demoProp reference
Full generated prop tables live in the repo; key components show props inline on docs pages.
Regenerate after interface changes: npm run docs:props · drift check: npm run test:props