skills/documentation/policyengine-design-skill/SKILL.md
PolicyEngine design system — tokens, typography, colors, charts, and branding for all project types. Triggers: "brand colors", "design tokens", "PolicyEngine colors", "typography", "font", "color palette", "CSS variables", "design system", "branding guidelines"
npx skillsauth add policyengine/policyengine-claude policyengine-designInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Single source of truth for PolicyEngine's visual identity. Design tokens are defined as CSS custom properties in @policyengine/ui-kit/theme.css. Every frontend project imports this single CSS file.
When to use which format:
| Context | Approach | Example |
|---------|----------|---------|
| React components | Tailwind semantic classes | className="bg-primary text-foreground" |
| Brand palette | Tailwind direct classes | className="bg-teal-500 text-gray-600" |
| Recharts (SVG) | CSS vars directly in fill/stroke | fill="var(--chart-1)" |
| Inline styles | CSS vars | style={{ color: "var(--primary)" }} |
| Python (Plotly) | Hex with CSS var comment | TEAL = "#319795" # --chart-1 |
| <meta> tags, static HTML | Hex values with CSS var name in comment | content="#319795" |
Python has no CSS runtime, so hex values are acceptable — but always comment with the CSS var name so values stay traceable to the design system.
Install:
bun install @policyengine/ui-kit
Import the theme CSS in your globals.css:
@import "tailwindcss";
@import "@policyengine/ui-kit/theme.css";
The first line enables Tailwind v4 utilities. The second provides all PE design tokens, @theme configuration, and base styles. Both are required — see policyengine-ui-kit-consumer-skill for details.
Source: PolicyEngine/policyengine-ui-kit/src/theme/tokens.css
The theme CSS has three layers:
:root — shadcn/ui semantic variables (--primary, --background, --chart-1, etc.)@theme inline — Bridges :root vars to Tailwind utilities (bg-primary, text-foreground)@theme — Brand palette (bg-teal-500, text-gray-600), font sizes, spacing, breakpoints| Token | Hex | Tailwind class | Usage |
|-------|-----|---------------|-------|
| teal-500 | #319795 | bg-teal-500 | Main brand color — charts, highlights |
| teal-400 | #38B2AC | bg-teal-400 | Lighter interactive elements |
| teal-600 | #2C7A7B | bg-teal-600 / bg-primary | Hover state, buttons |
| teal-700 | #285E61 | bg-teal-700 | Active/pressed state |
| teal-50 | #E6FFFA | bg-teal-50 | Tinted backgrounds |
| teal-800 | #234E52 | bg-teal-800 | Dark text on light teal |
| Role | CSS variable | Tailwind class | Hex |
|------|-------------|---------------|-----|
| Primary | --primary | bg-primary | #2C7A7B |
| Background | --background | bg-background | #FFFFFF |
| Foreground | --foreground | text-foreground | #000000 |
| Muted | --muted | bg-muted | #F2F4F7 |
| Muted foreground | --muted-foreground | text-muted-foreground | #6B7280 |
| Border | --border | border-border | #E2E8F0 |
| Destructive | --destructive | bg-destructive | #DC2626 |
| Card | --card | bg-card | #FFFFFF |
| Ring | --ring | ring-ring | #319795 |
| CSS variable | Tailwind class | Hex | Usage |
|-------------|---------------|-----|-------|
| --chart-1 | fill-chart-1 | #319795 | Primary series (teal) |
| --chart-2 | fill-chart-2 | #0EA5E9 | Secondary series (blue) |
| --chart-3 | fill-chart-3 | #285E61 | Tertiary series (dark teal) |
| --chart-4 | fill-chart-4 | #026AA2 | Quaternary series (dark blue) |
| --chart-5 | fill-chart-5 | #6B7280 | Quinary series (gray) |
These are the brand fill values — use for status dots, badges, and tinted surfaces. They are not WCAG-AA-compliant when set as text on a white background; for text, use the accessible-on-white variants below.
| Color | Hex | Tailwind class |
|-------|-----|---------------|
| Success | #22C55E | text-success / bg-success |
| Error | #DC2626 | text-destructive / bg-destructive |
| Warning | #FEC601 | text-warning / bg-warning |
| Info | #1890FF | text-info / bg-info |
Distinct from the brand fills above — these are the values you use when you actually need to render colored text on a white background and want to clear WCAG 2.2 AA (4.5:1 contrast at small text). Available since ui-kit 0.5.0 as --text-warning / --text-error / --text-success and the corresponding Tailwind utilities.
| Token | Hex | Tailwind class | Contrast on white |
|-------|-----|---------------|-------------------|
| --text-warning | #c2410c (Tailwind orange-700) | text-warning-foreground | 5.18:1 ✓ |
| --text-error | #B91C1C (Tailwind red-700) | text-error-foreground | 5.94:1 ✓ |
| --text-success | #285E61 (PE teal-700) | text-success-foreground | 7.07:1 ✓ |
The plain text-warning / text-destructive / text-success brand classes are intentionally not AA on white — they're meant for soft-tinted backgrounds and badge fills, not paragraph text.
Available since ui-kit 0.6.0. Activate by adding class="dark" to any ancestor element — no JS or media query required. Every shadcn semantic token (--primary, --background, --card, --border, etc.) and accessible text variant has a dark-mode value pinned to clear AA on the dark page background #0B0E14. Dark-mode values are tested by tests/theme/contrast.test.ts in ui-kit and exposed under the :root.dark selector in theme.css.
<!-- consumer site dark-mode toggle: just toggle the .dark class on <html> -->
<html class="dark">
...
</html>
If you're rendering charts, prefer the CSS-var form (fill="var(--chart-1)") over a hex literal — that way the chart picks up dark-mode swaps automatically. For Plotly / Python where there's no CSS runtime, use chartPalette.light and chartPalette.dark from @policyengine/ui-kit and pick by detected theme.
ui-kit's palette.gray is Slate-flavored (matches the dashboard's neutral surfaces and --border / --muted tokens). The legacy @policyengine/ui-kit/legacy/tokens shim still exports the Tailwind-3 grays (#6B7280 for 500, #4B5563 for 600) for design-system migration parity, but new code should use the canonical Slate values below.
| Token | Hex | Tailwind class |
|-------|-----|---------------|
| gray-50 | #F0F9FF | bg-gray-50 |
| gray-100 | #F2F4F7 | bg-gray-100 |
| gray-200 | #E2E8F0 | bg-gray-200 |
| gray-300 | #CBD5E1 | bg-gray-300 |
| gray-400 | #94A3B8 | bg-gray-400 |
| gray-500 | #64748B | text-gray-500 |
| gray-600 | #475569 | text-gray-600 |
| gray-700 | #344054 | text-gray-700 |
| gray-800 | #1E293B | text-gray-800 |
| gray-900 | #101828 | text-gray-900 |
Two font families only: Inter + JetBrains Mono. No serif fonts, no Roboto, no Public Sans.
| Context | Font | CSS variable | Tailwind |
|---------|------|-------------|----------|
| Everything (UI, charts, blog, tools) | Inter | --font-sans | font-sans |
| Code | JetBrains Mono | --font-mono | font-mono |
Loading Inter:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
| Tailwind class | Size | Usage |
|---------------|------|-------|
| text-xs | 12px | Small labels, captions |
| text-sm | 14px | Body text, form labels |
| text-base | 16px | Large body text |
| text-lg | 18px | Subheadings |
| text-xl | 20px | Section titles |
| text-2xl | 24px | Page titles |
| text-3xl | 28px | Large headings |
All UI text uses sentence case — capitalize only the first word and proper nouns.
Standard Tailwind spacing classes (p-4, gap-2, m-6) use the default Tailwind scale. Named spacing tokens:
| Token | Value | Tailwind class |
|-------|-------|---------------|
| Header | 58px | h-header |
| Sidebar | 280px | w-sidebar |
| Content | 976px | max-w-content |
| Tailwind class | Value |
|---------------|-------|
| rounded-sm | 4px |
| rounded-md | 6px |
| rounded-lg | 8px |
import { BarChart, Bar, XAxis, YAxis, Tooltip } from "recharts";
<BarChart data={data}>
<XAxis dataKey="name" niceTicks="snap125" domain={["auto", "auto"]} style={{ fontFamily: "var(--font-sans)" }} />
<YAxis niceTicks="snap125" domain={["auto", "auto"]} style={{ fontFamily: "var(--font-sans)" }} />
<Tooltip separator=": " />
<Bar dataKey="value" fill="var(--chart-1)" />
</BarChart>
SVG fill and stroke attributes accept var() directly — no helper function needed.
import plotly.graph_objects as go
TEAL = "#319795" # --chart-1
CHART_FONT = "Inter"
LOGO_URL = "https://raw.githubusercontent.com/PolicyEngine/policyengine-app-v2/main/app/public/assets/logos/policyengine/teal.png"
def format_fig(fig):
fig.update_layout(
font=dict(family=CHART_FONT, color="black", size=14),
plot_bgcolor="white",
paper_bgcolor="white",
template="plotly_white",
height=600,
width=800,
margin=dict(l=60, r=40, t=40, b=60),
modebar=dict(bgcolor="rgba(0,0,0,0)", color="rgba(0,0,0,0)"),
)
fig.add_layout_image(dict(
source=LOGO_URL,
xref="paper", yref="paper",
x=1.0, y=-0.10,
sizex=0.10, sizey=0.10,
xanchor="right", yanchor="bottom",
))
return fig
| Meaning | CSS variable | Hex |
|---------|-------------|-----|
| Positive / bonus / gains | --chart-1 | #319795 |
| Negative / penalty / losses | --chart-5 or --destructive | #6B7280 or #DC2626 |
| Neutral / baseline | --border | #E2E8F0 |
| Multi-series | --chart-1 through --chart-5 | See chart table above |
Inverted metrics (taxes): When a positive delta means bad (higher taxes), use invertDelta logic to show "Penalty" label and swap colors.
var(--font-sans), 14pxvar(--font-sans), 12pxvar(--font-sans), horizontal, above chartEvery PolicyEngine dashboard must include a favicon. The ui-kit exports the logo as a favicon-ready SVG:
cp node_modules/@policyengine/ui-kit/src/assets/logos/policyengine/teal-square.svg public/favicon.svglayout.tsx metadata:
export const metadata: Metadata = {
// ...
icons: { icon: '/favicon.svg' },
};
The ui-kit also exports logos.favicon (SVG) and logos.faviconPng (PNG fallback) for programmatic use.
All logo files in policyengine-app-v2/app/public/assets/logos/policyengine/:
| File | Background | Format |
|------|-----------|--------|
| teal.png / teal.svg | Light | Wide |
| teal-square.png / teal-square.svg | Light | Square (for chart watermarks) |
| white.png / white.svg | Dark | Wide |
| white-square.svg | Dark | Square |
Raw URL for charts:
https://raw.githubusercontent.com/PolicyEngine/policyengine-app-v2/main/app/public/assets/logos/policyengine/teal.png
| Project type | Token source | Font setup |
|-------------|-------------|------------|
| Standalone tool | @import "@policyengine/ui-kit/theme.css" | Google Fonts: Inter |
| app-v2 | import { colors } from '@/designTokens' | Built-in (Mantine + Inter) |
| Python chart | Hardcode or load tokens.json from @policyengine/design-system | Inter for Plotly |
| Blog HTML | Hardcode from token values | Google Fonts: Inter |
#319795 on white passes WCAG AA for large text (3.8:1)text-foreground (#000000) on white passes AAA (21:1)text-muted-foreground (#6B7280) on white passes AA (4.6:1)policyengine-interactive-tools-skill — Building standalone tools that use these tokenspolicyengine-vercel-deployment-skill — Deploying standalone toolspolicyengine-app-skill — app-v2 developmentpolicyengine-writing-skill — Content style (complements visual style)development
ALWAYS LOAD THIS SKILL for PolicyEngine PR reviews, including when the user invokes $review-program or Codex /review on a PolicyEngine PR. Performs read-only code validation, source-reference checks, regulatory review, optional PDF audit, summary reporting, and optional GitHub comment posting.
development
Use when the user invokes $fix-pr or asks Codex to apply fixes to a PolicyEngine PR based on $review-program findings, GitHub review comments, CI failures, or local review reports.
development
Use when the user invokes $encode-policy-v2 or asks Codex to implement a new PolicyEngine-US state benefit program from official rules. Covers research, source collection, requirement extraction, scoped implementation, tests, validation, and draft PR preparation.
development
Deploying PolicyEngine frontend apps to Vercel - naming, scope, team settings