Layout

How screens, frames and auto layout map to TSX and Tailwind.

Frame sizes

Frame Size in px Use
Desktop 1440 wide, height to fit Websites and landing pages
Desktop app 1440 by 900, or 1280 by 800 Web app screens and dashboards
Tablet 834 by 1194, or 1194 by 834 iPad portrait and landscape
Mobile 390 by 844 iPhone screens and mobile web
Android 412 by 915 Android screens
Slide 1920 by 1080 Presentations
Social 1080 by 1080, or 1200 by 630 Posts and link previews
A4 page 794 by 1123 Print documents and letters
A5 page 559 by 794 Flyers and small booklets
US Letter 816 by 1056 Print documents in the US
  • A website frame has a width and no height, so it grows with its content.
  • An app screen has a width and a height. Put overflow-hidden on its root, so that nothing goes outside the frame.
  • A mobile screen starts its content at the top: the navigation bar or the large title is first, with normal padding (pt-3 or pt-4). Do not draw a status bar (the time, signal, Wi-Fi or battery), and do not keep an empty band for one at the top (no pt-[59px], no empty spacer). Keep 34 px at the bottom for the home indicator. A tab bar is 83 px high with the home indicator. A navigation bar is 44 px high.
  • For a print page, use AddScreen(name, tsx, { paper: 'A4' }) (or 'A5', 'Letter'). It sets the page size and puts the page margin p-[20mm] on the root, unless the root has its own padding. Write print sizes in print units: text-[10pt], pt-[22mm]. Export it with Export(page.item, 'pdf', { page: 'A4' }).
  • Put frames in a row with 100 px between them. Put the desktop frame first and the mobile frame to its right.

Rules

  • Start the root of a screen with flex size-full flex-col bg-background text-foreground.
  • Use flex for rows and stacks. Use grid for columns. Use absolute only for overlays and decoration.
  • Put page content in a column: mx-auto w-full max-w-6xl px-6.
  • Give sections a rhythm: py-16 @3xl:py-24.
  • Write the narrow layout first, then add @3xl: and @5xl: classes for tablet and desktop. One component then works in the 390 px frame and in the 1440 px frame.
  • Make a bar chart from flex items with heights. Make a line chart from an inline <svg> with a polyline.
<svg
  viewBox="0 0 300 100"
  preserveAspectRatio="none"
  aria-hidden="true"
  className="h-40 w-full text-accent"
>
  <polyline
    points="0,80 40,70 80,74 120,50 160,56 200,30 240,36 300,12"
    fill="none"
    stroke="currentColor"
    strokeWidth={2}
    vectorEffect="non-scaling-stroke"
  />
</svg>

Recipes

Each recipe is a complete source. Copy the parts that you need. With execute, use Define for the components and AddScreen and Insert for the screen.

Landing page

A marketing page with nav, hero, logos, features, pricing, questions, a call to action and a footer. The same component fills a 1440 px frame and a 390 px frame.

import type { ReactNode } from 'react'

type ButtonProps = {
  label?: string
  tone?: 'primary' | 'secondary'
  size?: 'md' | 'lg'
  className?: string
}

const buttonTone = {
  primary: 'bg-accent text-accent-foreground',
  secondary: 'bg-surface text-foreground ring-1 ring-border'
}

const buttonSize = {
  md: 'h-9 px-4 text-sm',
  lg: 'h-12 px-6 text-base'
}

export function Button({
  label = 'Get started',
  tone = 'primary',
  size = 'lg',
  className = ''
}: ButtonProps) {
  return (
    <a
      href="#"
      className={`inline-flex items-center justify-center rounded-lg font-medium ${buttonTone[tone]} ${buttonSize[size]} ${className}`}
    >
      {label}
    </a>
  )
}

type FeatureProps = { title?: string; body?: string; icon?: ReactNode; className?: string }

export function Feature({
  title = 'Fast',
  body = 'Pages load in under a second.',
  icon,
  className = ''
}: FeatureProps) {
  return (
    <div className={`flex flex-col gap-3 ${className}`}>
      <div className="flex size-10 items-center justify-center rounded-lg bg-muted text-accent">
        {icon}
      </div>
      <h3 className="text-lg font-semibold">{title}</h3>
      <p className="text-pretty text-muted-foreground">{body}</p>
    </div>
  )
}

export function IconBolt({ className = '' }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={`size-5 ${className}`}
    >
      <path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z" />
    </svg>
  )
}

export function IconLock({ className = '' }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={`size-5 ${className}`}
    >
      <rect x="3" y="11" width="18" height="11" rx="2" />
      <path d="M7 11V7a5 5 0 0 1 10 0v4" />
    </svg>
  )
}

export function IconChart({ className = '' }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={`size-5 ${className}`}
    >
      <path d="M3 3v16a2 2 0 0 0 2 2h16" />
      <path d="M18 17V9" />
      <path d="M13 17V5" />
      <path d="M8 17v-3" />
    </svg>
  )
}

type PriceCardProps = {
  plan?: string
  price?: string
  blurb?: string
  featured?: boolean
  className?: string
}

export function PriceCard({
  plan = 'Pro',
  price = '$24',
  blurb = 'For growing teams.',
  featured = false,
  className = ''
}: PriceCardProps) {
  return (
    <div
      className={`flex flex-col gap-6 rounded-2xl p-8 ${featured ? 'bg-foreground text-background' : 'bg-surface ring-1 ring-border'} ${className}`}
    >
      <div className="flex flex-col gap-2">
        <h3 className="text-sm font-medium">{plan}</h3>
        <p className="text-4xl font-semibold tracking-tight">{price}</p>
        <p className="opacity-70">{blurb}</p>
      </div>
      {featured && <Button label="Start free trial" className="w-full" />}
      {!featured && <Button label="Choose plan" tone="secondary" className="w-full" />}
    </div>
  )
}

export function Landing({ className = '' }: { className?: string }) {
  return (
    <main className={`flex size-full flex-col bg-background text-foreground ${className}`}>
      <header data-name="Nav" className="flex h-16 items-center justify-between px-6 @5xl:px-12">
        <span className="text-lg font-semibold tracking-tight">Ledger</span>
        <nav className="hidden items-center gap-8 text-sm text-muted-foreground @3xl:flex">
          <a href="#">Product</a>
          <a href="#">Pricing</a>
          <a href="#">Customers</a>
          <a href="#">Docs</a>
        </nav>
        <Button label="Sign in" tone="secondary" size="md" />
      </header>

      <section
        data-name="Hero"
        className="flex flex-col items-center gap-6 px-6 pt-16 pb-20 text-center @3xl:pt-24 @5xl:px-12"
      >
        <p className="rounded-full bg-muted px-3 py-1 text-sm text-muted-foreground">
          New: payouts in 40 countries
        </p>
        <h1 className="max-w-4xl font-display text-5xl font-semibold tracking-tight text-balance @3xl:text-7xl">
          Payments that grow with your business
        </h1>
        <p className="max-w-2xl text-lg text-pretty text-muted-foreground">
          One account for invoices, cards and payouts, with prices that scale with you.
        </p>
        <div className="flex flex-col gap-3 @md:flex-row">
          <Button label="Start free" />
          <Button label="Talk to sales" tone="secondary" />
        </div>
        <div
          data-name="Product shot"
          className="mt-8 aspect-video w-full max-w-5xl rounded-2xl bg-muted ring-1 ring-border"
        />
      </section>

      <section
        data-name="Logos"
        className="flex flex-col items-center gap-6 border-y border-border px-6 py-10"
      >
        <p className="text-sm text-muted-foreground">Finance teams at these companies use Ledger</p>
        <div className="flex flex-wrap items-center justify-center gap-x-12 gap-y-4 text-lg font-semibold text-muted-foreground">
          <span>Northwind</span>
          <span>Harbour</span>
          <span>Kestrel</span>
          <span>Meridian</span>
          <span>Orbit</span>
        </div>
      </section>

      <section
        data-name="Features"
        className="mx-auto grid w-full max-w-6xl grid-cols-1 gap-10 px-6 py-20 @3xl:grid-cols-3 @3xl:py-28"
      >
        <Feature
          title="Paid in days"
          body="Send invoices that customers pay by card or bank transfer."
          icon={<IconBolt />}
        />
        <Feature
          title="Safe by default"
          body="Two-factor sign-in and audit logs on every plan."
          icon={<IconLock />}
        />
        <Feature
          title="Know your numbers"
          body="Live reports on cash flow, overdue invoices and payouts."
          icon={<IconChart />}
        />
      </section>

      <section
        data-name="Pricing"
        className="flex flex-col items-center gap-12 bg-surface px-6 py-20 @3xl:py-28"
      >
        <div className="flex max-w-2xl flex-col items-center gap-4 text-center">
          <h2 className="text-4xl font-semibold tracking-tight">Simple pricing</h2>
          <p className="text-lg text-muted-foreground">Start free. Pay when you grow.</p>
        </div>
        <div className="grid w-full max-w-6xl grid-cols-1 gap-6 @3xl:grid-cols-3">
          <PriceCard plan="Starter" price="$0" blurb="For one person with a few clients." />
          <PriceCard
            plan="Pro"
            price="$24"
            blurb="For small teams that send invoices every week."
            featured
          />
          <PriceCard plan="Business" price="$64" blurb="For companies with many teams." />
        </div>
      </section>

      <section data-name="Questions" className="mx-auto flex w-full max-w-3xl flex-col px-6 py-20">
        <h2 className="pb-6 text-3xl font-semibold tracking-tight">Questions</h2>
        <details className="border-b border-border py-5" open>
          <summary className="cursor-pointer font-medium">How long does it take to start?</summary>
          <p className="mt-3 text-muted-foreground">
            Most teams send their first invoice on the first day.
          </p>
        </details>
        <details className="border-b border-border py-5">
          <summary className="cursor-pointer font-medium">Can I cancel at any time?</summary>
          <p className="mt-3 text-muted-foreground">
            Yes. Your plan stops at the end of the month.
          </p>
        </details>
      </section>

      <section data-name="Call to action" className="px-6 pb-20">
        <div className="mx-auto flex max-w-6xl flex-col items-center gap-6 rounded-3xl bg-accent px-6 py-16 text-center text-accent-foreground">
          <h2 className="text-4xl font-semibold tracking-tight text-balance">
            Send your first invoice today
          </h2>
          <Button label="Start free" tone="secondary" />
        </div>
      </section>

      <footer
        data-name="Footer"
        className="grid grid-cols-2 gap-8 border-t border-border px-6 py-12 text-sm @3xl:grid-cols-4 @5xl:px-12"
      >
        <div className="col-span-2 flex flex-col gap-2 @3xl:col-span-1">
          <span className="font-semibold">Ledger</span>
          <span className="text-muted-foreground">© 2026 Ledger Pty Ltd</span>
        </div>
        <div className="flex flex-col gap-2 text-muted-foreground">
          <span className="font-medium text-foreground">Product</span>
          <a href="#">Invoices</a>
          <a href="#">Payouts</a>
        </div>
        <div className="flex flex-col gap-2 text-muted-foreground">
          <span className="font-medium text-foreground">Company</span>
          <a href="#">About</a>
          <a href="#">Careers</a>
        </div>
        <div className="flex flex-col gap-2 text-muted-foreground">
          <span className="font-medium text-foreground">Legal</span>
          <a href="#">Privacy</a>
          <a href="#">Terms</a>
        </div>
      </footer>
    </main>
  )
}

export const canvas = {
  pages: [
    {
      name: 'Web',
      items: [
        { render: Landing, x: 0, y: 0, width: 1440 },
        { render: Landing, x: 1540, y: 0, width: 390 }
      ]
    },
    {
      name: 'Components',
      kind: 'components',
      items: [
        { render: Button, x: 0, y: 0 },
        { render: Feature, x: 0, y: 100 },
        { render: PriceCard, x: 400, y: 0 }
      ]
    }
  ]
}

Dashboard shell

A web app screen with a sidebar, a top bar, a row of metrics, a bar chart and a list. It is 1440 by 900.

type NavItemProps = { label?: string; active?: boolean; className?: string }

export function NavItem({ label = 'Overview', active = false, className = '' }: NavItemProps) {
  return (
    <a
      href="#"
      className={`flex h-9 items-center rounded-md px-3 text-sm ${active ? 'bg-muted font-medium text-foreground' : 'text-muted-foreground'} ${className}`}
    >
      {label}
    </a>
  )
}

type MetricProps = {
  label?: string
  value?: string
  change?: string
  trend?: 'up' | 'down'
  className?: string
}

const metricTrend = {
  up: 'text-emerald-600',
  down: 'text-red-600'
}

export function Metric({
  label = 'Revenue',
  value = '$48,210',
  change = '+12.4% on last month',
  trend = 'up',
  className = ''
}: MetricProps) {
  return (
    <div
      className={`flex flex-col gap-2 rounded-lg bg-surface p-5 ring-1 ring-border ${className}`}
    >
      <p className="text-sm text-muted-foreground">{label}</p>
      <p className="text-2xl font-semibold tracking-tight">{value}</p>
      <p className={`text-sm ${metricTrend[trend]}`}>{change}</p>
    </div>
  )
}

export function Dashboard({ className = '' }: { className?: string }) {
  return (
    <div
      className={`grid size-full grid-cols-[240px_1fr] overflow-hidden bg-background text-foreground ${className}`}
    >
      <aside
        data-name="Sidebar"
        className="flex flex-col gap-6 border-r border-border bg-surface p-4"
      >
        <span className="px-3 pt-2 text-lg font-semibold tracking-tight">Ledger</span>
        <nav className="flex flex-col gap-1">
          <NavItem label="Overview" active />
          <NavItem label="Invoices" />
          <NavItem label="Customers" />
          <NavItem label="Payouts" />
          <NavItem label="Reports" />
        </nav>
        <nav className="mt-auto flex flex-col gap-1">
          <NavItem label="Settings" />
          <NavItem label="Help" />
        </nav>
      </aside>

      <div className="flex min-w-0 flex-col">
        <header
          data-name="Top bar"
          className="flex h-16 shrink-0 items-center justify-between border-b border-border px-8"
        >
          <h1 className="text-lg font-semibold">Overview</h1>
          <div className="flex items-center gap-3">
            <div className="flex h-9 w-72 items-center rounded-md bg-muted px-3 text-sm text-muted-foreground">
              Search invoices
            </div>
            <div className="size-8 rounded-full bg-muted" />
          </div>
        </header>

        <main className="flex min-h-0 flex-1 flex-col gap-6 p-8">
          <div data-name="Metrics" className="grid grid-cols-4 gap-4">
            <Metric label="Revenue" value="$48,210" change="+12.4% on last month" />
            <Metric label="Outstanding" value="$9,340" change="-3.1% on last month" trend="down" />
            <Metric label="Customers" value="1,284" change="+48 this month" />
            <Metric label="Paid on time" value="94%" change="+2 points" />
          </div>

          <div className="grid grid-cols-[2fr_1fr] gap-4">
            <section
              data-name="Revenue chart"
              className="flex flex-col gap-6 rounded-lg bg-surface p-5 ring-1 ring-border"
            >
              <div className="flex items-center justify-between">
                <h2 className="font-semibold">Revenue</h2>
                <span className="text-sm text-muted-foreground">Last 8 weeks</span>
              </div>
              <div className="flex h-64 items-end gap-3">
                <div className="h-[45%] flex-1 rounded-t-md bg-accent/25" />
                <div className="h-[60%] flex-1 rounded-t-md bg-accent/25" />
                <div className="h-[52%] flex-1 rounded-t-md bg-accent/25" />
                <div className="h-[70%] flex-1 rounded-t-md bg-accent/25" />
                <div className="h-[66%] flex-1 rounded-t-md bg-accent/25" />
                <div className="h-[80%] flex-1 rounded-t-md bg-accent/25" />
                <div className="h-[74%] flex-1 rounded-t-md bg-accent/25" />
                <div className="h-[92%] flex-1 rounded-t-md bg-accent" />
              </div>
            </section>

            <section
              data-name="Activity"
              className="flex flex-col gap-2 rounded-lg bg-surface p-5 ring-1 ring-border"
            >
              <h2 className="font-semibold">Activity</h2>
              <ul className="flex flex-col divide-y divide-border text-sm">
                <li className="flex justify-between gap-4 py-3">
                  <span>Northwind paid INV-1042</span>
                  <span className="text-muted-foreground">2m</span>
                </li>
                <li className="flex justify-between gap-4 py-3">
                  <span>Payout of $4,800 sent</span>
                  <span className="text-muted-foreground">1h</span>
                </li>
                <li className="flex justify-between gap-4 py-3">
                  <span>Kestrel viewed INV-1039</span>
                  <span className="text-muted-foreground">3h</span>
                </li>
                <li className="flex justify-between gap-4 py-3">
                  <span>INV-1031 is overdue</span>
                  <span className="text-muted-foreground">1d</span>
                </li>
              </ul>
            </section>
          </div>
        </main>
      </div>
    </div>
  )
}

export const canvas = {
  pages: [
    { name: 'App', items: [{ render: Dashboard, x: 0, y: 0, width: 1440, height: 900 }] },
    {
      name: 'Components',
      kind: 'components',
      items: [
        { render: NavItem, x: 0, y: 0 },
        { render: Metric, x: 0, y: 80 }
      ]
    }
  ]
}

Form

A settings form in a 640 px column. The Field component puts a label, a control and a hint together.

import type { ReactNode } from 'react'

type FieldProps = { label?: string; hint?: string; children?: ReactNode; className?: string }

export function Field({ label = 'Label', hint = '', children, className = '' }: FieldProps) {
  return (
    <label className={`flex flex-col gap-2 ${className}`}>
      <span className="text-sm font-medium">{label}</span>
      {children}
      {hint && <span className="text-sm text-muted-foreground">{hint}</span>}
    </label>
  )
}

export function Settings({ className = '' }: { className?: string }) {
  return (
    <main
      className={`flex size-full justify-center bg-background px-6 py-16 text-foreground ${className}`}
    >
      <form className="flex w-full max-w-160 flex-col gap-8">
        <div className="flex flex-col gap-1">
          <h1 className="text-2xl font-semibold tracking-tight">Profile</h1>
          <p className="text-muted-foreground">Other people see this on your invoices.</p>
        </div>

        <div className="flex flex-col gap-6 rounded-xl bg-surface p-6 ring-1 ring-border">
          <div className="grid grid-cols-1 gap-6 @xl:grid-cols-2">
            <Field label="First name">
              <input
                type="text"
                defaultValue="Ada"
                className="h-10 rounded-md bg-background px-3 ring-1 ring-border"
              />
            </Field>
            <Field label="Last name">
              <input
                type="text"
                defaultValue="Lovelace"
                className="h-10 rounded-md bg-background px-3 ring-1 ring-border"
              />
            </Field>
          </div>
          <Field label="Email" hint="Receipts go to this address.">
            <input
              type="email"
              defaultValue="ada@example.com"
              className="h-10 rounded-md bg-background px-3 ring-1 ring-border"
            />
          </Field>
          <Field label="Country">
            <select
              defaultValue="au"
              className="h-10 rounded-md bg-background px-3 ring-1 ring-border"
            >
              <option value="au">Australia</option>
              <option value="nz">New Zealand</option>
              <option value="gb">United Kingdom</option>
            </select>
          </Field>
          <Field label="Bio" hint="Up to 160 characters.">
            <textarea
              rows={3}
              defaultValue="Mathematician and writer."
              className="rounded-md bg-background px-3 py-2 ring-1 ring-border"
            />
          </Field>
        </div>

        <div className="flex flex-col gap-4 rounded-xl bg-surface p-6 ring-1 ring-border">
          <h2 className="font-semibold">Notifications</h2>
          <label className="flex items-center gap-3 text-sm">
            <input type="checkbox" defaultChecked className="size-4 accent-accent" />
            Email me when an invoice is paid
          </label>
          <label className="flex items-center gap-3 text-sm">
            <input type="checkbox" className="size-4 accent-accent" />
            Email me a summary each week
          </label>
          <div className="flex items-center justify-between text-sm">
            <span>Push notifications</span>
            <span
              data-name="Switch"
              className="flex h-6 w-11 items-center rounded-full bg-accent p-0.5"
            >
              <span className="ml-auto size-5 rounded-full bg-white shadow-sm" />
            </span>
          </div>
        </div>

        <div className="flex justify-end gap-3">
          <button
            type="button"
            className="h-10 rounded-lg px-4 text-sm font-medium ring-1 ring-border"
          >
            Cancel
          </button>
          <button
            type="button"
            className="h-10 rounded-lg bg-accent px-4 text-sm font-medium text-accent-foreground"
          >
            Save changes
          </button>
        </div>
      </form>
    </main>
  )
}

export const canvas = {
  pages: [
    { name: 'Settings', items: [{ render: Settings, x: 0, y: 0, width: 1440 }] },
    { name: 'Components', kind: 'components', items: [{ render: Field, x: 0, y: 0 }] }
  ]
}

Show a switch as two elements: the track and the knob. The knob is on the right when the switch is on.

Table

A table of invoices with a status badge. The badge colours are literal Tailwind colours, so they stay the same in dark mode.

type BadgeProps = { label?: string; status?: 'paid' | 'pending' | 'overdue'; className?: string }

const badgeStatus = {
  paid: 'bg-emerald-500/10 text-emerald-700',
  pending: 'bg-amber-500/10 text-amber-700',
  overdue: 'bg-red-500/10 text-red-700'
}

export function Badge({ label = 'Paid', status = 'paid', className = '' }: BadgeProps) {
  return (
    <span
      className={`inline-flex h-6 items-center rounded-full px-2.5 text-xs font-medium ${badgeStatus[status]} ${className}`}
    >
      {label}
    </span>
  )
}

export function Invoices({ className = '' }: { className?: string }) {
  return (
    <main
      className={`flex size-full flex-col gap-6 bg-background p-8 text-foreground ${className}`}
    >
      <div className="flex items-end justify-between">
        <div className="flex flex-col gap-1">
          <h1 className="text-2xl font-semibold tracking-tight">Invoices</h1>
          <p className="text-sm text-muted-foreground">128 invoices and $9,340 outstanding</p>
        </div>
        <button
          type="button"
          className="h-9 rounded-lg bg-accent px-4 text-sm font-medium text-accent-foreground"
        >
          New invoice
        </button>
      </div>

      <div className="overflow-hidden rounded-lg bg-surface ring-1 ring-border">
        <table className="w-full text-left text-sm">
          <thead className="bg-muted text-muted-foreground">
            <tr>
              <th className="h-10 px-4 font-medium">Invoice</th>
              <th className="h-10 px-4 font-medium">Customer</th>
              <th className="h-10 px-4 font-medium">Due</th>
              <th className="h-10 px-4 font-medium">Status</th>
              <th className="h-10 px-4 text-right font-medium">Amount</th>
            </tr>
          </thead>
          <tbody className="divide-y divide-border">
            <tr>
              <td className="h-12 px-4 font-mono text-xs">INV-1042</td>
              <td className="h-12 px-4">Northwind Traders</td>
              <td className="h-12 px-4 text-muted-foreground">3 Oct 2026</td>
              <td className="h-12 px-4">
                <Badge label="Paid" status="paid" />
              </td>
              <td className="h-12 px-4 text-right tabular-nums">$1,200.00</td>
            </tr>
            <tr>
              <td className="h-12 px-4 font-mono text-xs">INV-1041</td>
              <td className="h-12 px-4">Harbour Studio</td>
              <td className="h-12 px-4 text-muted-foreground">8 Oct 2026</td>
              <td className="h-12 px-4">
                <Badge label="Pending" status="pending" />
              </td>
              <td className="h-12 px-4 text-right tabular-nums">$3,450.00</td>
            </tr>
            <tr>
              <td className="h-12 px-4 font-mono text-xs">INV-1039</td>
              <td className="h-12 px-4">Kestrel Freight</td>
              <td className="h-12 px-4 text-muted-foreground">22 Sep 2026</td>
              <td className="h-12 px-4">
                <Badge label="Overdue" status="overdue" />
              </td>
              <td className="h-12 px-4 text-right tabular-nums">$640.00</td>
            </tr>
            <tr>
              <td className="h-12 px-4 font-mono text-xs">INV-1036</td>
              <td className="h-12 px-4">Meridian Health</td>
              <td className="h-12 px-4 text-muted-foreground">18 Sep 2026</td>
              <td className="h-12 px-4">
                <Badge label="Paid" status="paid" />
              </td>
              <td className="h-12 px-4 text-right tabular-nums">$12,980.00</td>
            </tr>
          </tbody>
        </table>
      </div>
    </main>
  )
}

export const canvas = {
  pages: [
    { name: 'App', items: [{ render: Invoices, x: 0, y: 0, width: 1440, height: 900 }] },
    { name: 'Components', kind: 'components', items: [{ render: Badge, x: 0, y: 0 }] }
  ]
}
  • Keep the cells of one column the same: the same padding, the same alignment and the same text style.
  • Align numbers to the right, with tabular-nums.
  • Use a row height of 40 to 48 px, and a header row with a muted fill or a bottom border.

Mobile app with a tab bar

An iPhone screen of 390 by 844 with a large title at the top, a grouped list, a tab bar and the home indicator. The second frame shows it in dark mode.

import type { ReactNode } from 'react'

type ListRowProps = {
  title?: string
  detail?: string
  time?: string
  unread?: boolean
  className?: string
}

export function ListRow({
  title = 'Invoice paid',
  detail = 'Northwind paid $1,200',
  time = '9:12',
  unread = false,
  className = ''
}: ListRowProps) {
  return (
    <div className={`flex min-h-16 items-center gap-3 px-4 py-3 ${className}`}>
      <span className={`size-2 shrink-0 rounded-full ${unread ? 'bg-accent' : 'bg-transparent'}`} />
      <div className="flex min-w-0 flex-1 flex-col">
        <span className="truncate text-[17px] font-semibold">{title}</span>
        <span className="truncate text-[15px] text-muted-foreground">{detail}</span>
      </div>
      <span className="text-[15px] text-muted-foreground">{time}</span>
    </div>
  )
}

type TabItemProps = { label?: string; active?: boolean; icon?: ReactNode; className?: string }

export function TabItem({ label = 'Inbox', active = false, icon, className = '' }: TabItemProps) {
  return (
    <a
      href="#"
      className={`flex flex-1 flex-col items-center gap-1 pt-2 text-[10px] font-medium ${active ? 'text-accent' : 'text-muted-foreground'} ${className}`}
    >
      {icon}
      {label}
    </a>
  )
}

export function IconInbox({ className = '' }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={`size-6 ${className}`}
    >
      <polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
      <path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
    </svg>
  )
}

export function IconSearch({ className = '' }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={`size-6 ${className}`}
    >
      <circle cx="11" cy="11" r="8" />
      <path d="m21 21-4.3-4.3" />
    </svg>
  )
}

export function IconBell({ className = '' }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={`size-6 ${className}`}
    >
      <path d="M10.268 21a2 2 0 0 0 3.464 0" />
      <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" />
    </svg>
  )
}

export function IconUser({ className = '' }: { className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={`size-6 ${className}`}
    >
      <path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
      <circle cx="12" cy="7" r="4" />
    </svg>
  )
}

export function Inbox({ className = '' }: { className?: string }) {
  return (
    <div
      className={`relative flex size-full flex-col overflow-hidden bg-background text-foreground ${className}`}
    >
      <header className="px-4 pt-4 pb-3">
        <h1 className="text-[34px] leading-[41px] font-bold tracking-tight">Inbox</h1>
      </header>
      <div data-name="Content" className="flex min-h-0 flex-1 flex-col gap-6 overflow-hidden">
        <section className="mx-4 flex flex-col divide-y divide-border overflow-hidden rounded-xl bg-surface">
          <ListRow title="Invoice paid" detail="Northwind paid $1,200" time="9:12" unread />
          <ListRow title="Payout sent" detail="$4,800 to your bank account" time="8:30" unread />
          <ListRow
            title="Invoice overdue"
            detail="Kestrel owes $640, due 2 days ago"
            time="Yesterday"
          />
          <ListRow title="New customer" detail="Harbour Studio signed up" time="Mon" />
        </section>
      </div>
      <nav
        data-name="Tab bar"
        className="flex h-[83px] shrink-0 border-t border-border bg-surface px-2 pb-[34px]"
      >
        <TabItem label="Inbox" active icon={<IconInbox />} />
        <TabItem label="Search" icon={<IconSearch />} />
        <TabItem label="Alerts" icon={<IconBell />} />
        <TabItem label="Profile" icon={<IconUser />} />
      </nav>
      <div
        data-name="Home indicator"
        className="absolute bottom-2 left-1/2 h-[5px] w-[134px] -translate-x-1/2 rounded-full bg-foreground"
      />
    </div>
  )
}

export const canvas = {
  pages: [
    {
      name: 'iPhone',
      items: [
        { render: Inbox, x: 0, y: 0, width: 390, height: 844 },
        { render: Inbox, x: 490, y: 0, width: 390, height: 844, modes: { theme: 'dark' } }
      ]
    },
    {
      name: 'Components',
      kind: 'components',
      items: [
        { render: ListRow, x: 0, y: 0 },
        { render: TabItem, x: 0, y: 100 }
      ]
    }
  ]
}
  • Keep touch targets 44 px or larger.
  • Use iOS type sizes: large title 34, title 28, headline 17 semibold, body 17, subhead 15, footnote 13, caption 12.
  • Keep 16 px of padding on the sides.