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-hiddenon 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-3orpt-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 (nopt-[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 marginp-[20mm]on the root, unless the root has its own padding. Write print sizes in print units:text-[10pt],pt-[22mm]. Export it withExport(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
absoluteonly 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 apolyline.
<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.