How-to

FramingUI Builds a Dashboard Layout with AI

A practical walkthrough of composing an admin dashboard from FramingUI components, showing how AI tools generate consistent code when tokens are in place.

FramingUI Team6 min read

Building a Dashboard Layout with FramingUI and AI

Dashboard layouts are where design token discipline pays off most visibly. A sidebar, a header, a grid of stat cards, and a data table—each built separately—either cohere or they do not. When AI tools generate these components without a design system, the inconsistencies compound fast.

This walkthrough shows how to build each layer of a dashboard using FramingUI components, and how to structure the project so AI-assisted code stays consistent with the rest of the interface.

Setting Up

Install the packages:

pnpm add @framingui/ui @framingui/core @framingui/tokens tailwindcss-animate

For AI tool integration, set up the MCP server:

npx -y @framingui/mcp-server@latest init

This command bootstraps the full runtime contract: provider setup, CSS imports, and Claude Code MCP configuration. After restarting Claude Code, AI-generated components will reference the same design tokens as the components you write manually.

Layout Shell

The outer structure gives the layout its shape. A fixed sidebar on the left, a flex column on the right holding the header and scrollable content:

import { Separator } from '@framingui/ui'

export function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen">
      <aside className="w-64 border-r border-[var(--border-default)] bg-[var(--bg-card)]">
        <div className="p-[var(--spacing-6)]">
          <h2 className="text-lg font-bold">MyApp</h2>
        </div>
        <Separator />
      </aside>

      <div className="flex-1 flex flex-col">
        <header className="h-16 border-b border-[var(--border-default)] bg-[var(--bg-card)] flex items-center px-[var(--spacing-6)]">
          <h1 className="text-xl font-semibold">Dashboard</h1>
        </header>
        <main className="flex-1 overflow-auto p-[var(--spacing-6)] bg-[var(--bg-background)]">
          {children}
        </main>
      </div>
    </div>
  )
}

All spacing and border values come from CSS custom properties. When the theme changes, every dimension updates automatically.

The sidebar needs active state tracking. Using the FramingUI Button component with variant switching keeps the active item visually distinct without custom CSS:

'use client'

import { useState } from 'react'
import { Home, Users, BarChart3, Settings, ChevronDown } from 'lucide-react'
import { Button, Separator } from '@framingui/ui'
import { cn } from '@framingui/ui/lib/utils'

const navItems = [
  { icon: Home,     label: 'Dashboard', href: '/dashboard' },
  { icon: Users,    label: 'Users',     href: '/users' },
  { icon: BarChart3, label: 'Analytics', href: '/analytics' },
  { icon: Settings, label: 'Settings',  href: '/settings' },
]

export function DashboardSidebar() {
  const [activeItem, setActiveItem] = useState('/dashboard')

  return (
    <aside className="w-64 border-r border-[var(--border-default)] bg-[var(--bg-card)] flex flex-col h-screen">
      <div className="p-[var(--spacing-6)]">
        <h2 className="text-lg font-bold">MyApp Admin</h2>
      </div>
      <Separator />

      <nav className="flex-1 p-[var(--spacing-4)] space-y-[var(--spacing-2)]">
        {navItems.map((item) => (
          <Button
            key={item.href}
            variant={activeItem === item.href ? 'default' : 'ghost'}
            className={cn(
              'w-full justify-start',
              activeItem === item.href && 'bg-[var(--bg-primary)] text-[var(--bg-primary-foreground)]'
            )}
            onClick={() => setActiveItem(item.href)}
          >
            <item.icon className="mr-[var(--spacing-2)] h-4 w-4" />
            {item.label}
          </Button>
        ))}
      </nav>

      <Separator />
      <div className="p-[var(--spacing-4)]">
        <Button variant="ghost" className="w-full justify-between">
          <div className="flex items-center">
            <div className="w-8 h-8 rounded-full bg-[var(--bg-primary)] flex items-center justify-center text-sm font-bold text-[var(--bg-primary-foreground)] mr-[var(--spacing-3)]">
              JD
            </div>
            <div className="text-left">
              <p className="text-sm font-medium">John Doe</p>
              <p className="text-xs text-[var(--bg-muted-foreground)]">Admin</p>
            </div>
          </div>
          <ChevronDown className="h-4 w-4" />
        </Button>
      </div>
    </aside>
  )
}

Stats Card Grid

The stat grid demonstrates why token-based spacing matters. Every gap, padding, and icon background uses the same design token scale, so the cards feel like a cohesive system rather than individually styled elements:

import { Card, CardContent } from '@framingui/ui'
import { Users, DollarSign, ShoppingCart, TrendingUp } from 'lucide-react'
import { cn } from '@framingui/ui/lib/utils'

const stats = [
  { title: 'Total Revenue',     value: '$45,231', change: '+20.1%', icon: DollarSign,  trend: 'up'   },
  { title: 'Total Users',       value: '2,350',   change: '+12.5%', icon: Users,       trend: 'up'   },
  { title: 'Total Orders',      value: '1,234',   change: '-4.3%',  icon: ShoppingCart, trend: 'down' },
  { title: 'Conversion Rate',   value: '3.24%',   change: '+0.5%',  icon: TrendingUp,  trend: 'up'   },
] as const

export function StatsGrid() {
  return (
    <div className="grid gap-[var(--spacing-4)] md:grid-cols-2 lg:grid-cols-4">
      {stats.map((stat) => (
        <Card key={stat.title}>
          <CardContent className="p-[var(--spacing-6)]">
            <div className="flex items-center justify-between space-x-[var(--spacing-4)]">
              <div className="space-y-[var(--spacing-2)]">
                <p className="text-sm font-medium text-[var(--bg-muted-foreground)]">
                  {stat.title}
                </p>
                <div className="flex items-baseline space-x-[var(--spacing-2)]">
                  <p className="text-2xl font-bold">{stat.value}</p>
                  <span className={cn(
                    'text-xs font-medium',
                    stat.trend === 'up' ? 'text-[var(--bg-primary)]' : 'text-[var(--bg-destructive)]'
                  )}>
                    {stat.change}
                  </span>
                </div>
              </div>
              <div className="p-[var(--spacing-3)] bg-[var(--bg-primary)]/10 rounded-[var(--radius-lg)]">
                <stat.icon className="h-5 w-5 text-[var(--bg-primary)]" />
              </div>
            </div>
          </CardContent>
        </Card>
      ))}
    </div>
  )
}

The responsive grid handles column count automatically: one column on mobile, two on tablet, four on desktop.

Data Table

Wrapping the table in a Card keeps it visually consistent with the stat cards above. Status badges use semantic variants that the design system maps to appropriate colors:

import {
  Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
  Card, CardHeader, CardTitle, CardContent, Badge, Button,
} from '@framingui/ui'
import { MoreHorizontal } from 'lucide-react'

const orders = [
  { id: '#3210', customer: 'Alice Johnson', amount: '$250.00', status: 'completed', date: '2026-02-27' },
  { id: '#3211', customer: 'Bob Smith',     amount: '$120.50', status: 'pending',   date: '2026-02-27' },
  { id: '#3212', customer: 'Carol White',   amount: '$89.99',  status: 'completed', date: '2026-02-26' },
  { id: '#3213', customer: 'David Brown',   amount: '$340.00', status: 'processing', date: '2026-02-26' },
]

export function RecentOrdersTable() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Recent Orders</CardTitle>
      </CardHeader>
      <CardContent>
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead>Order ID</TableHead>
              <TableHead>Customer</TableHead>
              <TableHead>Amount</TableHead>
              <TableHead>Status</TableHead>
              <TableHead>Date</TableHead>
              <TableHead className="w-[50px]" />
            </TableRow>
          </TableHeader>
          <TableBody>
            {orders.map((order) => (
              <TableRow key={order.id}>
                <TableCell className="font-medium">{order.id}</TableCell>
                <TableCell>{order.customer}</TableCell>
                <TableCell>{order.amount}</TableCell>
                <TableCell>
                  <Badge variant={
                    order.status === 'completed' ? 'default' :
                    order.status === 'pending'   ? 'secondary' : 'outline'
                  }>
                    {order.status}
                  </Badge>
                </TableCell>
                <TableCell className="text-[var(--bg-muted-foreground)]">{order.date}</TableCell>
                <TableCell>
                  <Button variant="ghost" size="icon">
                    <MoreHorizontal className="h-4 w-4" />
                  </Button>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </CardContent>
    </Card>
  )
}

Mobile Sidebar

The sidebar needs a drawer variant for small screens. FramingUI's Sheet component handles the overlay:

'use client'

import { useState } from 'react'
import { Menu } from 'lucide-react'
import { Button, Sheet, SheetContent, SheetTrigger } from '@framingui/ui'

export function MobileSidebar({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false)

  return (
    <>
      <div className="lg:hidden">
        <Sheet open={open} onOpenChange={setOpen}>
          <SheetTrigger asChild>
            <Button variant="ghost" size="icon">
              <Menu className="h-5 w-5" />
            </Button>
          </SheetTrigger>
          <SheetContent side="left" className="p-0 w-64">
            {children}
          </SheetContent>
        </Sheet>
      </div>
      <div className="hidden lg:block">{children}</div>
    </>
  )
}

Using AI to Extend the Dashboard

With MCP configured, you can ask Claude Code to generate additional dashboard sections and the output will reference the same tokens as the components above. A prompt like this works well:

Add a revenue chart section to the dashboard.
Use FramingUI Card for the wrapper.
Use var(--bg-primary) for the chart line color.
Use var(--spacing-6) for all internal padding.
Use Recharts for the chart library.

The specific token references in the prompt constrain the AI's output. Without them, the AI might pick stroke="#3b82f6" or padding: "24px"—values that look right but break when the theme changes.

Composing the Final Page

import { DashboardSidebar } from './dashboard-sidebar'
import { StatsGrid }        from './stats-grid'
import { RecentOrdersTable } from './recent-orders-table'
import { Button }            from '@framingui/ui'

export default function DashboardPage() {
  return (
    <div className="flex h-screen">
      <DashboardSidebar />
      <div className="flex-1 flex flex-col">
        <header className="h-16 border-b border-[var(--border-default)] bg-[var(--bg-card)] flex items-center justify-between px-[var(--spacing-6)]">
          <h1 className="text-xl font-semibold">Dashboard Overview</h1>
          <Button variant="default">Download Report</Button>
        </header>
        <main className="flex-1 overflow-auto p-[var(--spacing-6)] bg-[var(--bg-background)]">
          <div className="space-y-[var(--spacing-6)]">
            <StatsGrid />
            <RecentOrdersTable />
          </div>
        </main>
      </div>
    </div>
  )
}

Each section was built independently and composed here. Because every component references the same token set, the visual result is coherent without explicit coordination between the components.

The practical benefit shows up when you add features. A new section follows the same pattern—import from @framingui/ui, use CSS custom properties for all style values—and it matches the rest of the dashboard automatically.

Ready to build with FramingUI?

Build consistent UI with AI-ready design tokens. No more hallucinated colors or spacing.

Try FramingUI
Share

Related Posts