Agent skill
policyengine-frontend-builder-spec
Mandatory frontend technology requirements for PolicyEngine dashboards and interactive tools — Tailwind CSS v4, Next.js (App Router), @policyengine/ui-kit theme, Vercel deployment
Install this agent skill to your Project
npx add-skill https://github.com/PolicyEngine/policyengine-claude/tree/main/skills/tools-and-apis/policyengine-frontend-builder-spec-skill
SKILL.md
Frontend builder spec
Authoritative specification for all PolicyEngine frontend projects (dashboards and interactive tools). Any agent building or validating a frontend MUST load this skill and follow every requirement below. Where another agent's instructions conflict with this spec, this spec wins.
Mandatory requirements
1. Tailwind CSS (v4+)
The application MUST use Tailwind CSS v4 for all styling. Tailwind utility classes are the primary styling mechanism.
- MUST install
tailwindcss(v4+) - MUST have a
globals.csscontaining:css@import "tailwindcss"; @import "@policyengine/ui-kit/theme.css"; - MUST NOT have a
tailwind.config.tsortailwind.config.js— Tailwind v4 uses@themein CSS instead - MUST NOT have a
postcss.config.jsorpostcss.config.mjs— Tailwind v4 does not require PostCSS - MUST NOT use
@tailwind base; @tailwind components; @tailwind utilities;— use@import "tailwindcss"instead - MUST NOT use plain CSS files or CSS modules (
*.module.css) for layout or styling - MUST NOT use other CSS-in-JS libraries (styled-components, emotion, vanilla-extract)
- MUST NOT use other component frameworks for styling (Mantine, Chakra UI, Material UI)
- The only CSS files allowed are
globals.css(which imports ui-kit theme)
2. @policyengine/ui-kit (component library + theme)
The application MUST install @policyengine/ui-kit and use it as the primary component library and design token source. MUST use ui-kit components when an equivalent exists — do NOT rebuild components that ui-kit already provides.
- MUST install:
bun add @policyengine/ui-kit - MUST import theme in
globals.css:@import "@policyengine/ui-kit/theme.css"; - MUST use ui-kit components for all standard UI patterns (see availability table below)
- MAY build custom components only when no ui-kit equivalent exists
Component availability table:
| Dashboard need | ui-kit component |
|---|---|
| Page shell | DashboardShell |
| Header with logo + nav | Header (light/dark variants, navLinks prop) |
| Two-column layout | SidebarLayout + InputPanel + ResultsPanel |
| Single-column narrative | SingleColumnLayout |
| Buttons | Button (4 variants, 3 sizes) |
| Cards | Card, CardHeader, CardTitle, CardContent, CardFooter |
| Badges | Badge (6 variants) |
| Tab navigation | Tabs, TabsList, TabsTrigger, TabsContent |
| Currency input | CurrencyInput |
| Number input | NumberInput |
| Select dropdown | SelectInput |
| Checkbox | CheckboxInput |
| Slider | SliderInput |
| Input grouping | InputGroup |
| KPI display | MetricCard (currency/percent, trends) |
| Summary text | SummaryText |
| Data tables | DataTable |
| Charts | ChartContainer, PEBarChart, PELineChart, PEAreaChart, PEWaterfallChart |
| Branding | PolicyEngineWatermark, logos.* |
| Utilities | formatCurrency, formatPercent, formatNumber |
Component precedence rule: When building UI:
- First: Use
@policyengine/ui-kitif it has the component - Second: Use shadcn/ui primitives (Dialog, Popover, Tooltip, Select, DropdownMenu, etc.) styled with Tailwind semantic classes
- Third: Build custom from scratch with Tailwind utility classes
3. Design tokens via ui-kit theme
The application MUST load design tokens from @policyengine/ui-kit/theme.css. This single CSS import provides all colors, spacing, typography, and chart tokens.
- MUST import theme in
globals.css:@import "@policyengine/ui-kit/theme.css"; - MUST NOT load tokens via CDN
<link>— the theme is bundled with ui-kit - MUST NOT hardcode hex color values when a design token exists
- MUST NOT hardcode pixel spacing values when a Tailwind spacing class exists
- MUST NOT hardcode font-family values — use
var(--font-sans) - MAY use custom values when no token covers the need (e.g., chart-specific dimensions, animation durations)
Token usage patterns:
| 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)" }} |
4. Framework: Next.js (App Router)
The application MUST use Next.js with the App Router.
- MUST use
create-next-appor equivalent to scaffold with App Router - MUST have
next.config.tsat the project root - MUST have an
app/directory withlayout.tsxandpage.tsx - MUST use TypeScript (
.ts/.tsxfiles,tsconfig.json) - MUST NOT use the Pages Router (
pages/directory) - MUST NOT use Vite as the application bundler (Vite is only used by Vitest for testing)
- MUST NOT use other bundlers (Webpack, Parcel, esbuild, etc.)
- MUST NOT use other meta-frameworks (Remix, Gatsby, Astro, etc.)
5. Package manager: bun
The application MUST use bun as the package manager.
- MUST use
bun installinstead ofnpm install - MUST use
bun run dev,bun run buildinstead ofnpm run dev,npm run build - MUST use
bunx vitest runinstead ofnpx vitest run - MUST have a
bun.locklockfile (notpackage-lock.json) - MUST NOT use npm, yarn, or pnpm
6. Vercel deployment
The application MUST be deployed using Vercel.
- MUST have a
vercel.jsonat the project root with appropriate configuration - MUST use
output: 'export'innext.config.tsfor static export, unless the dashboard requires server-side rendering - MUST configure the Vercel project to build from the repository root (not a subdirectory)
- MUST set any required environment variables in the Vercel project settings using the
NEXT_PUBLIC_*prefix - MUST deploy under the
policy-engineVercel scope - MUST NOT deploy using other hosting platforms (Netlify, AWS Amplify, GitHub Pages, etc.) for the frontend
7. shadcn/ui for custom components
When building custom components not available in @policyengine/ui-kit, the application SHOULD use shadcn/ui primitives as the base layer.
- SHOULD initialize shadcn/ui:
bunx shadcn@latest init - SHOULD use shadcn/ui for: Dialog, Popover, Tooltip, Select, DropdownMenu, Accordion, Sheet, and other interaction primitives
- MUST style shadcn/ui components with Tailwind semantic classes (the ui-kit theme already defines shadcn/ui semantic tokens like
background,foreground,primary,muted) - MUST NOT use shadcn/ui when an equivalent
@policyengine/ui-kitcomponent exists
Tailwind v4 + ui-kit theme integration pattern
globals.css
@import "tailwindcss";
@import "@policyengine/ui-kit/theme.css";
body {
font-family: var(--font-sans);
color: var(--foreground);
background: var(--background);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
The single @import "@policyengine/ui-kit/theme.css" provides:
:rootvariables — shadcn/ui semantic tokens (--primary,--background,--chart-1, etc.)@theme inline— Bridges:rootvars to Tailwind utilities (bg-primary,text-foreground)@theme— Brand palette (bg-teal-500,text-gray-600), font sizes, spacing, breakpoints
Next.js: app/layout.tsx
import './globals.css'
import { Inter } from 'next/font/google'
import type { Metadata } from 'next'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'DASHBOARD_TITLE - PolicyEngine',
description: 'DASHBOARD_DESCRIPTION',
icons: { icon: '/favicon.svg' },
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
Usage in components
// Prefer ui-kit components:
import { MetricCard, Button, Card, CardContent } from '@policyengine/ui-kit';
import { formatCurrency } from '@policyengine/ui-kit';
// Use Tailwind classes with semantic and brand tokens for custom layouts:
<div className="bg-background border border-border rounded-lg p-4 flex flex-col gap-1">
<span className="text-sm text-muted-foreground font-medium">Metric title</span>
<span className="text-2xl font-bold text-foreground">{formatCurrency(1234)}</span>
</div>
// Brand colors when needed:
<div className="bg-teal-500 text-white hover:bg-teal-600">Primary teal</div>
// Responsive design uses Tailwind breakpoint prefixes:
<main className="max-w-content mx-auto px-6 py-4 font-sans text-foreground">
<div className="flex gap-6 md:flex-col">
{/* sidebar collapses at md breakpoint */}
</div>
</main>
// Recharts uses CSS vars directly:
<Line stroke="var(--chart-1)" />
<Bar fill="var(--chart-2)" />
<CartesianGrid stroke="var(--border)" />
// Header with logo and structured nav links:
import { Header, logos } from '@policyengine/ui-kit';
<Header
variant="dark"
logo={<img src={logos.whiteWordmark} alt="PolicyEngine" className="h-5" />}
navLinks={[
{ slug: 'research', text: 'Research', href: 'https://policyengine.org/research' },
]}
>
<span className="ml-2">Dashboard Title</span>
</Header>
Project structure
DASHBOARD_NAME/
├── app/
│ ├── layout.tsx # Root layout — Inter font + globals.css
│ ├── page.tsx # Main dashboard page
│ ├── globals.css # @import "tailwindcss" + @import ui-kit theme
│ └── providers.tsx # React Query provider (client component)
├── components/
│ └── (from plan.yaml) # Custom dashboard components (only if not in ui-kit)
├── lib/
│ ├── api/
│ │ ├── client.ts # API client (stubs or real)
│ │ ├── types.ts # Request/response TypeScript types
│ │ └── fixtures.ts # Mock data for stubs
│ ├── embedding.ts # Country detection, hash sync, share URLs
│ └── hooks/
│ └── useCalculation.ts # React Query hooks
├── public/
├── next.config.ts
├── tsconfig.json
├── package.json
├── vitest.config.ts
├── plan.yaml
├── CLAUDE.md
├── README.md
├── vercel.json
└── .gitignore
Package dependencies
Production:
nextreact,react-domtailwindcss(v4+)@policyengine/ui-kit@tanstack/react-queryrecharts(if custom charts beyond ui-kit)react-plotly.js,plotly.js-dist-min(if maps)axios
Development:
typescript,@types/react,@types/react-dom,@types/nodevitest,@vitejs/plugin-react,jsdom@testing-library/react,@testing-library/jest-dom
Testing
Vitest is the test runner. Configure vitest.config.ts:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
},
})
Note: @vitejs/plugin-react is only used by Vitest for JSX transform during testing — Vite is NOT used as the application bundler.
What NOT to do
- MUST NOT use Vite as the application bundler — only Next.js is allowed (Vite is used only by Vitest for testing)
- MUST NOT use other bundlers (Webpack, Parcel, esbuild)
- MUST NOT use other meta-frameworks (Remix, Gatsby, Astro)
- MUST NOT use the Next.js Pages Router — use App Router only
- MUST NOT have
tailwind.config.tsorpostcss.config.js— Tailwind v4 uses@themein CSS - MUST NOT use
@tailwind base; @tailwind components; @tailwind utilities;— use@import "tailwindcss" - MUST NOT use plain CSS files or CSS modules for layout/styling
- MUST NOT use styled-components, emotion, or vanilla-extract
- MUST NOT use Mantine, Chakra UI, Material UI, or other component frameworks for styling
- MUST NOT hardcode hex color values when a design token exists
- MUST NOT hardcode pixel spacing values when a Tailwind spacing class exists
- MUST NOT hardcode font-family values — use
var(--font-sans)via Tailwind - MUST NOT deploy to platforms other than Vercel
- MUST NOT use npm, yarn, or pnpm — use bun
- MUST NOT rebuild components that exist in
@policyengine/ui-kit - MUST NOT use
getCssVar()— it no longer exists. SVG acceptsvar()directly.
Related skills
- policyengine-design-skill — Complete design token reference (colors, typography, spacing, chart branding)
- policyengine-interactive-tools-skill — Embedding, hash sync, country detection patterns
- policyengine-app-skill — app-v2 component architecture reference
Didn't find tool you were looking for?