skills/frontend/policyengine-ui-kit-consumer-skill/SKILL.md
This skill should be used when setting up a new project that uses @policyengine/ui-kit, debugging CSS or styling issues in a consumer app, or when Tailwind utility classes are not being generated. Also use when creating globals.css, configuring PostCSS, or troubleshooting "no styles", "no spacing", or "no layout" problems. Triggers: "ui-kit import", "globals.css setup", "Tailwind not working", "styles not applying", "utility classes missing", "setup ui-kit", "PostCSS config", "no styling", "CSS broken", "import ui-kit", "theme.css", "no layout", "no spacing", "@tailwindcss/postcss", "PolicyEngineShell", "multizone shell", "PolicyEngine header", "PolicyEngine footer"
npx skillsauth add policyengine/policyengine-claude policyengine-ui-kit-consumerInstall 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.
How to correctly import and use the PolicyEngine UI kit's design system in any consumer application. This skill covers the required setup, the correct import order, and common mistakes that cause styling to break.
Every app using @policyengine/ui-kit needs exactly three things:
bun add @policyengine/ui-kit
bun add -D @tailwindcss/postcss postcss
postcss.config.mjsexport default {
plugins: {
"@tailwindcss/postcss": {},
},
};
No other PostCSS plugins needed — @tailwindcss/postcss handles imports, vendor prefixes, and nesting internally.
app/globals.css with two imports@import "tailwindcss";
@import "@policyengine/ui-kit/theme.css";
Both lines are required. The order matters. Tailwind must come first because the ui-kit's @theme blocks extend it.
This provides:
flex, grid, p-4, text-sm, etc.)bg-primary, text-foreground, border-border)bg-teal-500, text-gray-600, bg-blue-500)Multizone apps must render the PolicyEngine shell themselves. The parent app-v2 rewrite cannot inject a header or footer into a child app response, and iframes should not be used to fake a shared shell.
For Next App Router apps, prefer the runtime shell exports from ui-kit instead of copying header constants into each repo:
import { PolicyEngineShell } from "@policyengine/ui-kit";
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<PolicyEngineShell country="us">{children}</PolicyEngineShell>
</body>
</html>
);
}
Use country="uk" for UK-only apps. If the app needs a custom main wrapper or
sticky local toolbar, render the header and footer separately:
import { PolicyEngineFooter, PolicyEngineHeader } from "@policyengine/ui-kit";
import "./globals.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<PolicyEngineHeader country="uk" />
{children}
<PolicyEngineFooter country="uk" />
</body>
</html>
);
}
When adding a PE header above an app-local sticky bar, offset the local sticky bar by the shared header height:
.local-toolbar {
position: sticky;
top: var(--spacing-header, 58px);
}
Static HTML routes still need visible PE branding and nav. Prefer migrating the route to the canonical Next stack; if that is not feasible, include a small static shell in the generated HTML. Do not rely on app-v2 rewrites, iframes, or the parent route to add the shell after the fact.
For multizone apps, verify both the destination URL and the policyengine.org source URL. The source URL is the one users and ads see.
Understanding the flow prevents debugging confusion:
globals.css through @tailwindcss/postcss@import "tailwindcss" establishes the cascade layers and enables utility class generationprocess.cwd() (the consumer's project root) — this is why the consumer's utility classes get generated@import "@policyengine/ui-kit/theme.css" is inlined by Tailwind's import bundler@theme and @theme inline blocks merge into the consumer's Tailwind build@source directive tells Tailwind to also scan the ui-kit's own component files@layer base styles apply within the existing cascade/* WRONG — utility classes will not be generated */
@import "@policyengine/ui-kit/theme.css";
/* CORRECT */
@import "tailwindcss";
@import "@policyengine/ui-kit/theme.css";
Without @import "tailwindcss", there is no Tailwind build. The ui-kit's @theme blocks have nothing to extend. No utility classes (flex, p-4, grid) will exist.
/* WRONG — double Tailwind causes conflicting resets and broken styles */
@import "tailwindcss";
@import "@policyengine/ui-kit/theme.css";
@import "tailwindcss";
The ui-kit does NOT contain @import "tailwindcss" inside it. One import at the top of globals.css is all that's needed.
/* WRONG — Tailwind v4 does not use JavaScript config */
tailwind.config.ts ← DELETE THIS
Tailwind v4 is CSS-first. All configuration comes from @theme blocks in the ui-kit's theme CSS. There is no content array, no theme.extend, no JavaScript config.
/* WRONG — these conflict with @tailwindcss/postcss */
export default {
plugins: {
"postcss-import": {},
"@tailwindcss/postcss": {},
"autoprefixer": {},
},
};
/* CORRECT — @tailwindcss/postcss handles both internally */
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
@import "tailwindcss" inside the ui-kit packageIf working on the ui-kit itself, never add @import "tailwindcss" to tokens.css. The consumer owns that import. See tailwind-design-system-authoring skill for details.
/* WRONG */
<div style={{ color: '#319795', fontFamily: 'Inter' }}>
/* CORRECT — use Tailwind classes */
<div className="text-teal-500 font-sans">
/* CORRECT — use CSS variables for inline styles */
<div style={{ color: 'var(--primary)', fontFamily: 'var(--font-sans)' }}>
globals.css has @import "tailwindcss" as the first linepostcss.config.mjs exists with @tailwindcss/postcss@tailwindcss/postcss and postcss are installed as devDependenciesglobals.css is imported in app/layout.tsx (or pages/_app.tsx)This means @theme tokens are being processed but Tailwind's utility generation isn't scanning files correctly.
If missing classes are from the consumer's own components (app/, components/):
@import "tailwindcss" comes BEFORE the ui-kit import (order matters)process.cwd() is the project root when the build runssource() to the import: @import "tailwindcss" source("./src")If missing classes are from ui-kit components (DashboardShell, Header, InputPanel, etc.):
The ui-kit's @source directive in tokens.css may not match the actual directory structure. This is a ui-kit-side fix — the @source glob must cover all directories containing .tsx files with className= attributes. See the tailwind-design-system-authoring skill for the verification procedure.
This means Tailwind is being imported twice.
tokens.css does NOT contain @import "tailwindcss"globals.css has only ONE @import "tailwindcss" lineThe ui-kit ships @source directives to tell Tailwind to scan its components. If this fails:
@source in globals.css:
@import "tailwindcss";
@import "@policyengine/ui-kit/theme.css";
@source "../node_modules/@policyengine/ui-kit/src";
bun link (symlinked package), the path resolves differently — check the actual resolved pathStandard setup. Requires @tailwindcss/postcss in PostCSS config.
app/
globals.css ← @import "tailwindcss"; @import ui-kit theme
layout.tsx ← import "./globals.css";
postcss.config.mjs
Same setup. Turbopack processes PostCSS normally. No changes needed.
Use @tailwindcss/vite instead of @tailwindcss/postcss:
// vite.config.ts
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ plugins: [tailwindcss()] })
No postcss.config.mjs needed — the Vite plugin handles everything.
globals.css is the same two imports.
| What | Where | Content |
|------|-------|---------|
| PostCSS config | postcss.config.mjs | { plugins: { "@tailwindcss/postcss": {} } } |
| Entry CSS | app/globals.css | @import "tailwindcss"; @import "@policyengine/ui-kit/theme.css"; |
| Dependencies | package.json devDeps | @tailwindcss/postcss, postcss |
| Dependencies | package.json deps | @policyengine/ui-kit |
After the two-line import, these are available:
| Category | Examples | Source |
|----------|---------|--------|
| Semantic colors | bg-primary, text-foreground, border-border | :root + @theme inline |
| Brand palette | bg-teal-500, text-gray-600, bg-blue-500 | @theme |
| Status colors | text-success, bg-warning, text-error | @theme |
| Chart colors | fill-chart-1 through fill-chart-5 | :root + @theme inline |
| Typography | text-sm (14px), text-base (16px), font-sans | @theme |
| Spacing | h-header (58px), w-sidebar (280px), max-w-content (976px) | @theme |
| Breakpoints | xs:, sm:, md:, lg:, xl:, 2xl: | @theme |
| Radius | rounded-sm (4px), rounded-md (6px), rounded-lg (8px) | @theme inline |
| All Tailwind utilities | flex, grid, p-4, gap-2, hidden, etc. | @import "tailwindcss" |
@policyengine/design-system@policyengine/design-system is deprecated — every new tool must use @policyengine/ui-kit. For repos still importing from design-system, ui-kit ships a backwards-compat shim under @policyengine/ui-kit/legacy/* that mirrors the design-system API exactly. Migrating is a pure import-path rename — no source rewrite needed.
| Before | After |
|---|---|
| from "@policyengine/design-system" | from "@policyengine/ui-kit/legacy" |
| from "@policyengine/design-system/tokens" | from "@policyengine/ui-kit/legacy/tokens" |
| from "@policyengine/design-system/tokens/colors" | from "@policyengine/ui-kit/legacy/tokens/colors" |
| from "@policyengine/design-system/tokens/typography" | from "@policyengine/ui-kit/legacy/tokens/typography" |
| from "@policyengine/design-system/tokens/spacing" | from "@policyengine/ui-kit/legacy/tokens/spacing" |
| from "@policyengine/design-system/charts" | from "@policyengine/ui-kit/legacy/charts" |
| next.config.* transpilePackages: ['@policyengine/design-system'] | transpilePackages: ['@policyengine/ui-kit'] |
| vitest.config.* inline: ['@policyengine/design-system'] | inline: ['@policyengine/ui-kit'] |
| HTML CDN unpkg.com/@policyengine/design-system/dist/tokens.css | unpkg.com/@policyengine/ui-kit/dist/styles.css |
| package.json deps "@policyengine/design-system": "^0.3.x" | "@policyengine/ui-kit": "^0.9.0" |
After the rename, prefer the canonical exports for new code (this gets you accessibility wins from 0.6.0+):
| Legacy | Canonical | Notes |
|---|---|---|
| colors.primary[N] | palette.teal[N] | same hex |
| colors.gray[N] | palette.gray[N] | DIFFERENT hex — legacy is Tailwind-3 gray, canonical is Slate |
| colors.blue[N] | palette.blue[N] | same hex |
| colors.warning | semanticFills.warning | same #FEC601 |
| colors.error | semanticFills.error | same #EF4444 |
| colors.text.warning | var(--text-warning) | DIFFERENT hex — legacy #d9480f fails AA at small text; canonical #c2410c clears 5.18:1 |
| typography.fontFamily.primary | typography.fontFamily.sans | same Inter stack |
| chartColors (Plotly) | chartPalette.light / chartPalette.dark | by-theme resolved hex |
Both colors.gray[N] and colors.text.warning change visible color on migration — don't bulk sed-replace, walk per usage.
@policyengine/ui-kit ships ESM + CJS, but the lockfile pattern across PolicyEngine is bun.lock committed (NOT in .gitignore). If your CI runs bun install --frozen-lockfile, make sure bun.lock is committed — otherwise the install always fails.tests/consumer-types/ harness type-checks the built dist/ surface against a bundler-resolution consumer. If your repo embeds a similar pattern (or just runs tsc --noEmit against node_modules/@policyengine/ui-kit), put bun run build before bun run test in the workflow.package.json lives in a subdirectory (app/, frontend/, etc.), set the Vercel project's Root Directory to that subdir in the dashboard — don't add a root-level vercel.json with cd subdir && bun install commands. The two configs fight and the framework detector fails ("No Next.js version detected").Header API changed in 0.4.0. Old props (variant, logo, navLinks, children) no longer exist. New API uses navItems, logoSrc, linkComponent. If you bump from ^0.3.x and hit Type '{ children: Element; variant: string; logo: Element; navLinks: …; }' is not assignable to type 'IntrinsicAttributes & HeaderProps', that's the migration. Read @policyengine/ui-kit/dist/layout/header/Header.d.ts for the current shape.For child apps served under policyengine.org, the target state is:
@import "tailwindcss"; before @import "@policyengine/ui-kit/theme.css";.@policyengine/ui-kit installed and used for the PE shell.framework: vite, stale outputDirectory, or root-level cd subdir && build commands).Research, Model, API, Donate).Use PolicyEngineShell for straightforward tools, and use
PolicyEngineHeader/PolicyEngineFooter separately when the page structure
needs a custom main container. Keep the shell runtime-owned by ui-kit; do not
copy nav arrays into new apps.
policyengine-design-skill — Full token reference (hex values, usage guidelines)policyengine-tailwind-shadcn-skill — @theme namespace mechanics, SVG var() usagepolicyengine-interactive-tools-skill — Full tool scaffolding checklistpolicyengine-vercel-deployment-skill — Deploying consumer appstools
ONLY use this skill when users explicitly ask about the PolicyEngine Python package installation, REST API endpoints, API authentication, rate limits, or policyengine.py client library. DO NOT use for household benefit/tax calculations — ALWAYS use policyengine-us or policyengine-uk instead. This skill is about the API/client tooling itself, not about calculating benefits or taxes.
development
ALWAYS USE THIS SKILL for PolicyEngine microsimulation, population-level analysis, winners/losers calculations. Triggers: microsimulation, share who would lose/gain, policy impact, national average, weighted analysis, cost, revenue impact, budgetary, estimate the cost, federal revenues, tax revenue, budget score, how much would it cost, how much would the policy cost, total cost of, aggregate impact, cost to the government, revenue loss, fiscal impact, poverty impact, child poverty, deep poverty, poverty rate, poverty reduction, how many people lifted out of poverty, SPM poverty, distributional impact, state tax, state-level, California, New York, UBI, universal basic income, flat tax, standard deduction, winners and losers, winners, losers, inequality, Gini, decile, SALT, marginal tax rate, effective tax rate. NOT for single-household calculations like "what would my benefit be" - use policyengine-us or policyengine-uk for those. Use this skill's code pattern; explore codebase for parameter paths if needed.
development
PolicyEngine API v2 - Next-generation microservices architecture with monorepo structure
development
ALWAYS LOAD THIS SKILL before setting up any Python environment or installing packages. Defines the standard: uv, Python 3.13, uv pip install, .venv at project root. Triggers: "set up python", "install python", "create a venv", "virtual environment", "pip install", "install packages", "uv pip", "uv venv", "python version", "VIRTUAL_ENV", "venv conflict", "which python", "activate", "deactivate", "run the script", "run with uv", "uv run", "pyproject.toml", "install dependencies", "install requirements", "install the package", "editable install", "pip install -e", "latest package", "latest version", "current version", "newest version".