| name | policyengine-design |
| description | 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"
|
PolicyEngine design system
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.
The ui-kit theme
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
Colors
Primary — teal
| 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 |
Semantic (shadcn/ui)
| 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 |
Charts
| 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) |
Additional semantic colors
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 |
Accessible-on-white text variants
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.
Dark mode
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.
<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.
Gray scale
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 |
Typography
Font families
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">
Font sizes
| 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 |
Sentence case
All UI text uses sentence case — capitalize only the first word and proper nouns.
- "Your saved policies" not "Your Saved Policies"
- "Tax liability by income" not "Tax Liability by Income"
- Proper nouns stay capitalized: "Child Tax Credit", "PolicyEngine", "California"
Spacing
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 |
Border radius
| Tailwind class | Value |
|---|
rounded-sm | 4px |
rounded-md | 6px |
rounded-lg | 8px |
Chart branding
Recharts (React tools)
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.
Plotly (Python)
import plotly.graph_objects as go
TEAL = "#319795"
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
Chart color conventions
| 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.
Chart typography
- Axis labels and titles:
var(--font-sans), 14px
- Tick labels:
var(--font-sans), 12px
- Legend:
var(--font-sans), horizontal, above chart
Favicon
Every PolicyEngine dashboard must include a favicon. The ui-kit exports the logo as a favicon-ready SVG:
- Copy:
cp node_modules/@policyengine/ui-kit/src/assets/logos/policyengine/teal-square.svg public/favicon.svg
- Add to
layout.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.
Logos
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
Using tokens by project type
| 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 |
Accessibility
- Teal
#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)
- Never rely on color alone — use labels, patterns, or position to convey meaning
- Ensure chart data series are distinguishable in grayscale
Related skills
policyengine-interactive-tools-skill — Building standalone tools that use these tokens
policyengine-vercel-deployment-skill — Deploying standalone tools
policyengine-app-skill — app-v2 development
policyengine-writing-skill — Content style (complements visual style)