Policyengine design
Skill PolicyEngine/policyengine-claude/skills/policyengine-design
Load when styling any PolicyEngine frontend — choosing colors, fonts, spacing, chart colors, or consuming @policyengine/ui-kit. Covers the two-import globals.css, the three-layer theme.css (@theme vs @theme inline), verified 0.4.0 token values and component exports, the dark-mode contract, and the Plotly chart house style. Triggers: PolicyEngine colors, design tokens, brand palette, teal, CSS variables, theme.css, ui-kit, @theme, @theme inline, dark mode, chart colors, fill var(--chart-1), Plotly format_fig, never hardcode hex, DashboardShell, Header. NOT for: multizone/deploy wiring (use policyengine-tools) or app-v2 internals (policyengine-app).From its SKILL.md
npx -y skills add PolicyEngine/policyengine-claude --skill policyengine-designAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
9.9 KB, as published. Nobody here has run it
PolicyEngine design system
Design tokens live as CSS custom properties in @policyengine/ui-kit/theme.css. Every
frontend imports that one file. The canonical version is @policyengine/ui-kit ^0.4.0 —
the version app-v2 pins. Ignore claims tied to 0.5/0.6/0.9; treat 0.4.0 as ground truth and
re-check node_modules/@policyengine/ui-kit when precision matters.
Setup: the two imports
/* globals.css — order matters; Tailwind must come first */
@import "tailwindcss";
@import "@policyengine/ui-kit/theme.css";
Next.js needs postcss.config.mjs = { plugins: { "@tailwindcss/postcss": {} } } (install
@tailwindcss/postcss + postcss). A Vite app uses the @tailwindcss/vite plugin instead —
no PostCSS config. Do not add postcss-import or autoprefixer (@tailwindcss/postcss
handles both), and do not add a second @import "tailwindcss" (theme.css does not import
it). If the consumer's own utility classes go missing in a monorepo, scope source detection:
@import "tailwindcss" source("./src");.
The three-layer theme.css
theme.css (source: ui-kit src/theme/tokens.css, vendored from design-system v0.3.0) has
three layers:
Layer 1 :root { --primary: #2C7A7B; --chart-1: #319795; ... } raw shadcn/ui semantic values
Layer 2 @theme inline { --color-primary: var(--primary); } bridge :root → Tailwind utilities
Layer 3 @theme { --color-teal-500: #319795; --text-sm: 14px; } brand palette, fonts, sizes, spacing
@theme vs @theme inline — the mechanism
@theme inlinekeeps thevar()reference, so the utility resolves it at runtime. Use it whenever a token points at a:rootvariable — that is what letsbg-primaryfollow a.darkoverride. All shadcn/ui semantic tokens use@theme inline.@themebakes a literal value into the generated CSS at build time. Use it for the fixed brand palette (teal-500is always#319795), font sizes, spacing, breakpoints.
Getting these backwards is the classic bug: @theme { --color-primary: var(--primary); } bakes
the literal string var(--primary) and never re-resolves.
Consuming tokens by context
| Context | How | Example |
|---|---|---|
| React (semantic) | Tailwind semantic class | className="bg-primary text-foreground" |
| React (brand) | Tailwind brand class | className="bg-teal-500 text-gray-600" |
| Recharts / SVG | var() directly in fill/stroke | fill="var(--chart-1)" |
| Inline style | var() | style={{ color: "var(--primary)" }} |
| Python / Plotly | hex + CSS-var comment | TEAL = "#319795" # --chart-1 |
Namespace → utility mapping (Tailwind v4): --color-* → bg-*/text-*/border-*/fill-*;
--text-* → font-size utilities; --font-* → font-*; --radius-* → rounded-*;
--spacing-* → p-*/m-*/gap-*/w-*/h-*; --breakpoint-* → sm:/md:/lg:.
Never hardcode hex or font names when a token exists.
Colors (verified 0.4.0)
Teal (brand): 50 #E6FFFA · 100 #B2F5EA · 200 #81E6D9 · 300 #4FD1C5 · 400 #38B2AC
· 500 #319795 (main brand) · 600 #2C7A7B (= --primary) · 700 #285E61 · 800 #234E52
· 900 #1D4044.
Semantic (:root): --primary #2C7A7B · --background #FFFFFF · --foreground
#000000 · --muted #F2F4F7 · --muted-foreground #475569 · --border #E2E8F0 ·
--card #FFFFFF · --destructive #EF4444 · --ring #319795.
Charts (--chart-1 … --chart-5): #319795 (teal) · #0EA5E9 (blue) · #285E61
(dark teal) · #026AA2 (dark blue) · #64748B (slate). Exactly five.
Status fills: success #22C55E · warning #FEC601 · error #EF4444 · info #1890FF.
These are fills for dots/badges/tints — not guaranteed AA as text on white.
Gray is Slate-flavored: 500 #64748B, 600 #475569, 700 #344054, 900 #101828.
Typography, spacing, radius
- Two fonts only: Inter (
--font-sans, everything) and JetBrains Mono (--font-mono, code). Load Inter via Google Fonts (family=Inter:wght@400;500;600;700&display=swap). - Sizes:
text-xs12 ·sm14 ·base16 ·lg18 ·xl20 ·2xl24 ·3xl28 ·4xl32 (each with a matching line-height). - Radius: base
--radius6px; semantic--radius-chip2 /--radius-element4 /--radius-container8 /--radius-feature12; Tailwindrounded-sm/md/lg/xl. - Named spacing:
--spacing-header58px (h-header),--spacing-sidebar280px (w-sidebar),--spacing-content976px (max-w-content). - Breakpoints: xs 36rem · sm 48rem · md 62rem · lg 75rem · xl 88rem · 2xl 96rem.
- Sentence case on all UI text — capitalize only the first word and proper nouns.
ui-kit component library (0.4.0)
@policyengine/ui-kit is a shadcn/radix component library plus the theme. Tokens are consumed
via CSS only — at 0.4.0 the package exports no JS palette/colors object and no
/legacy shim. Subpath exports: ./primitives, ./layout, ./inputs, ./display,
./charts, ./visualization, ./utils, ./assets, ./theme.css, ./styles.css.
- Layout/shell:
DashboardShell,SidebarLayout,SingleColumnLayout,InputPanel,ResultsPanel,Header,Footer,Stack,Group,Container. There is noPolicyEngineShell/PolicyEngineHeader/PolicyEngineFooter— those names do not exist; useDashboardShell/Header/Footer. HeaderAPI (0.4.0):navItems: {label, href, children?}[],logoSrc,logoHref,linkComponent,countries,currentCountry,onCountryChange. It renders a sticky teal-gradient bar. Oldervariant/logo/navLinks/childrenprops are gone.- Primitives:
Button,Card(+parts),Badge,Tabs,Dialog,Sheet,Tooltip,Select,DropdownMenu,Input,Alert,Popover,Switch,Checkbox,Accordion,SegmentedControl, … Inputs:CurrencyInput,NumberInput,SelectInput,CheckboxInput,SliderInput,InputGroup. Display:MetricCard,SummaryText,DataTable,PolicyEngineWatermark. Utils:formatCurrency,formatPercent,formatNumber,cn,logos(logos.tealWordmark/tealSquare/whiteWordmark/whiteSquare).
The favicon SVG ships at @policyengine/ui-kit/favicon.svg (teal square).
Dark mode (accurate 0.4.0 contract)
theme.css declares @custom-variant dark (&:is(.dark *)), so a .dark class on an ancestor
activates dark: utilities. But 0.4.0 ships no dark token values — there is no
.dark {} / :root.dark {} block redefining the semantic vars. Toggling .dark alone does
not recolor bg-primary/bg-background. A consumer that wants dark mode must define its
own overrides:
.dark { --background: #0B0E14; --foreground: #FFFFFF; --card: #111827; --border: #1F2937; }
Prefer the CSS-var form for charts (fill="var(--chart-1)") so they follow any dark override
automatically. Do not claim built-in dark mode at this version.
Charts
- Recharts / SVG: pass
var(--chart-1)…var(--chart-5)tofill/stroke; grid/chrome usevar(--border),var(--foreground),var(--font-sans). SVG resolvesvar()natively — no helper. Round ticks:niceTicks="snap125"on numeric axes (recharts ≥3.8, PolicyEngine-contributed; see the policyengine-tools skill for the version gate and thedomain={["auto", "auto"]}pairing). Conventions: gains--chart-1; losses--chart-5or--destructive; neutral--border. - Plotly (Python maps/charts) — house style: white
plot_bgcolor/paper_bgcolor, Inter font, teal series, logo bottom-right. Reference tokens by hex with a CSS-var comment. Sketch:
Use a local logo file copied into the repo — do not hotlink a raw GitHub URL.def format_fig(fig): fig.update_layout(font=dict(family="Inter", color="black", size=14), plot_bgcolor="white", paper_bgcolor="white", template="plotly_white") # add_layout_image with a CHECKED-IN local logo asset (xref/yref="paper", bottom-right). return fig
Migrating off the old design-system
<!-- stale-ok -->@policyengine/design-system is deprecated; use @policyengine/ui-kit. The migration is a
consumption-model change, not a rename: drop the old JS token imports and consume ui-kit's
CSS vars via theme.css (there is no ui-kit JS token object to import at 0.4.0). Watch the
gray shift — the old design-system used Tailwind-3 grays (#6B7280 at 500) while ui-kit uses
Slate (#64748B at 500), so grays change visibly; migrate per-usage, don't bulk-sed.
app-v2 keeps a local @/designTokens JS shim (its own colors/spacing/typography
objects mirroring theme.css) for inline styles during its migration — that shim is app-v2's,
not an export of ui-kit.
Common mistakes
- Creating
tailwind.config.ts— Tailwind v4 is CSS-first; all config is in@theme. getCssVar()/getComputedStyle()for chart colors — SVG takesvar()directly.- Old
pe-*-prefixed classes (bg-pe-primary-500) — usebg-teal-500/bg-primary. - Hardcoding hex in components — use the Tailwind class or
var().
Related skills
- policyengine-tools — building/deploying tools that consume these tokens
- policyengine-app — app-v2's ui-kit + local designTokens hybrid