Tutorial

Building Consistent UI with Claude

How to use FramingUI with Claude Code to generate pixel-perfect, consistent UI components every time.

FramingUI Team6 min read

The Claude Code Challenge

Claude Code is exceptional at understanding context and generating production-ready code. However, without explicit design constraints, it will:

  1. Invent color schemes — "I'll make this button blue" → bg-blue-600
  2. Guess spacing patterns — Mix p-4, px-5, gap-3 inconsistently
  3. Create one-off styles — Each component gets unique styling

The result? A codebase where every AI-generated component looks different.

FramingUI: Claude's Design Guardrails

FramingUI provides Claude with a structured design vocabulary through:

  • Type-safe components from @framingui/ui
  • Semantic design tokens like var(--tekton-bg-primary)
  • Consistent patterns enforced through component APIs

Setup: Teaching Claude About FramingUI

Step 1: Create a Project Knowledge File

Create .claude/project-knowledge.md:

# Project Design System

## FramingUI Component Library

This project uses **FramingUI**, a shadcn-ui fork with integrated design tokens.

### Available Components

Import from `@framingui/ui`:
- Layout: Card, Sheet, Dialog, Tabs, Separator
- Forms: Button, Input, Label, Checkbox, RadioGroup, Select, Switch, Textarea
- Data: Table, Avatar, Badge
- Feedback: Toast, Tooltip, Progress
- Navigation: Breadcrumb, NavigationMenu

### Design Tokens

All styling uses CSS custom properties:

**Colors:**
- `var(--tekton-bg-primary)` - Primary brand color
- `var(--tekton-bg-secondary)` - Secondary accent
- `var(--tekton-bg-destructive)` - Error/danger state
- `var(--tekton-bg-muted)` - Muted backgrounds
- `var(--tekton-bg-accent)` - Accent highlights
- `var(--tekton-bg-card)` - Card backgrounds

**Spacing:**
- `var(--tekton-spacing-1)` through `var(--tekton-spacing-8)`

**Border Radius:**
- `var(--tekton-radius-sm)`, `var(--tekton-radius-md)`, `var(--tekton-radius-lg)`, `var(--tekton-radius-xl)`

### Code Generation Rules

1. **Never hardcode colors** - Always use `var(--tekton-*)` tokens
2. **Import from @framingui/ui** - Not from local component files
3. **Use semantic names** - `primary`, not `blue`
4. **Prefer components over HTML** - Use `<Button>` instead of `<button>`

Step 2: Install FramingUI

pnpm add @framingui/ui @framingui/core

Step 3: Import Tokens

// app/layout.tsx
import '@framingui/ui/styles/tokens.css';

Example: Prompting Claude for Consistent UI

❌ Without FramingUI

Prompt:

Create a user profile card with name, email, and edit button

Claude generates:

export function ProfileCard({ name, email }: ProfileCardProps) {
  return (
    <div className="bg-white p-6 rounded-lg shadow-md border border-gray-200">
      <h3 className="text-xl font-bold text-gray-900">{name}</h3>
      <p className="text-gray-600 text-sm mt-1">{email}</p>
      <button className="mt-4 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
        Edit Profile
      </button>
    </div>
  );
}

Issues:

  • Hardcoded colors: bg-white, text-gray-900, bg-blue-600
  • Arbitrary spacing: p-6, px-4 py-2
  • Won't adapt to theme changes

✅ With FramingUI

Prompt:

Create a user profile card using FramingUI components with name, email, and edit button

Claude generates:

import { Card, CardHeader, CardTitle, CardContent, CardFooter, Button } from '@framingui/ui';

export function ProfileCard({ name, email }: ProfileCardProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{name}</CardTitle>
      </CardHeader>
      <CardContent>
        <p className="text-sm text-[var(--tekton-bg-muted-foreground)]">{email}</p>
      </CardContent>
      <CardFooter>
        <Button variant="default">Edit Profile</Button>
      </CardFooter>
    </Card>
  );
}

Benefits:

  • Uses FramingUI Card components
  • Semantic tokens: var(--tekton-bg-muted-foreground)
  • Automatically adapts to theme
  • Consistent with other components

Real-World Example: Building a Dashboard

Let's build a complete dashboard with Claude using FramingUI.

Prompt 1: Stats Cards

Create 4 stat cards showing Total Users, Revenue, Active Sessions, and Conversion Rate.
Use FramingUI Card component and design tokens.

Claude generates:

import { Card, CardContent } from '@framingui/ui';
import { Users, DollarSign, Activity, TrendingUp } from 'lucide-react';

const stats = [
  { title: 'Total Users', value: '12,345', icon: Users, change: '+12%' },
  { title: 'Revenue', value: '$54,321', icon: DollarSign, change: '+8%' },
  { title: 'Active Sessions', value: '1,234', icon: Activity, change: '+23%' },
  { title: 'Conversion Rate', value: '3.2%', icon: TrendingUp, change: '+0.5%' },
];

export function StatsGrid() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[var(--tekton-spacing-4)]">
      {stats.map((stat) => (
        <Card key={stat.title}>
          <CardContent className="p-[var(--tekton-spacing-6)]">
            <div className="flex items-center justify-between">
              <div>
                <p className="text-sm text-[var(--tekton-bg-muted-foreground)]">
                  {stat.title}
                </p>
                <p className="text-2xl font-bold mt-[var(--tekton-spacing-2)]">
                  {stat.value}
                </p>
                <p className="text-sm text-[var(--tekton-bg-primary)] mt-[var(--tekton-spacing-1)]">
                  {stat.change}
                </p>
              </div>
              <div className="p-[var(--tekton-spacing-3)] bg-[var(--tekton-bg-primary)]/10 rounded-[var(--tekton-radius-lg)]">
                <stat.icon className="w-6 h-6 text-[var(--tekton-bg-primary)]" />
              </div>
            </div>
          </CardContent>
        </Card>
      ))}
    </div>
  );
}

Notice: All spacing uses tokens, colors are semantic

Prompt 2: Data Table

Add a users table below the stats with columns: Name, Email, Role, Status.
Use FramingUI Table component.

Claude generates:

import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell, Badge } from '@framingui/ui';

const users = [
  { name: 'Alice Johnson', email: 'alice@example.com', role: 'Admin', status: 'active' },
  { name: 'Bob Smith', email: 'bob@example.com', role: 'User', status: 'active' },
  { name: 'Carol White', email: 'carol@example.com', role: 'User', status: 'inactive' },
];

export function UsersTable() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Recent Users</CardTitle>
      </CardHeader>
      <CardContent>
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>Name</TableHead>
              <TableHead>Email</TableHead>
              <TableHead>Role</TableHead>
              <TableHead>Status</TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {users.map((user) => (
              <TableRow key={user.email}>
                <TableCell className="font-medium">{user.name}</TableCell>
                <TableCell className="text-[var(--tekton-bg-muted-foreground)]">
                  {user.email}
                </TableCell>
                <TableCell>{user.role}</TableCell>
                <TableCell>
                  <Badge variant={user.status === 'active' ? 'default' : 'secondary'}>
                    {user.status}
                  </Badge>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </CardContent>
    </Card>
  );
}

Notice: Badge variants, semantic color tokens, consistent spacing

Advanced: Custom Token Extensions

You can guide Claude to use custom tokens for app-specific patterns:

/* globals.css */
@import '@framingui/ui/styles/tokens.css';

:root {
  --app-sidebar-width: 280px;
  --app-header-height: 64px;
}

Update project knowledge:

### Custom Tokens
- `--app-sidebar-width` - Sidebar width (280px)
- `--app-header-height` - Header height (64px)

Prompt:

Create a dashboard layout with sidebar and header using our custom tokens

Claude generates:

export function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen">
      <aside className="w-[var(--app-sidebar-width)] border-r border-[var(--tekton-border-default)]">
        {/* Sidebar content */}
      </aside>
      <div className="flex-1 flex flex-col">
        <header className="h-[var(--app-header-height)] border-b border-[var(--tekton-border-default)]">
          {/* Header content */}
        </header>
        <main className="flex-1 overflow-auto p-[var(--tekton-spacing-6)]">
          {children}
        </main>
      </div>
    </div>
  );
}

Tips for Better Claude + FramingUI Results

1. Be Specific About Components

❌ "Create a modal" ✅ "Create a modal using FramingUI Dialog component"

2. Reference Design Tokens in Prompts

❌ "Make the text gray" ✅ "Use var(--tekton-bg-muted-foreground) for the text color"

3. Ask for Semantic Variants

❌ "Make the button red" ✅ "Use Button variant='destructive'"

4. Iterate with Consistency Checks

Review the code and ensure all colors use var(--tekton-*) tokens

Measuring Success

After setting up FramingUI with Claude, audit your components:

# Check for hardcoded colors (should be 0)
grep -r "bg-blue\|text-red\|#[0-9A-F]\{6\}" src/

# Check for token usage (should be everywhere)
grep -r "var(--tekton-" src/

Next Steps

  1. Add project knowledge - Create .claude/project-knowledge.md
  2. Install FramingUI - pnpm add @framingui/ui
  3. Test with prompts - Ask Claude to generate components
  4. Verify consistency - Check that all components use tokens

With FramingUI, Claude Code becomes a design-system-aware code generator, producing consistent, production-ready UI every single time.

Ready to build with FramingUI?

Join the beta and get early access to agentic design systems that adapt to your needs.

Join Beta
Share

Related Posts