Internal · v0.1.0
A lightweight, purpose-built component library for the Accounts Payable product. Tokens, components, and patterns designed to serve clarity over cleverness.
Consistent
Shared tokens across every surface — no magic numbers.
Accessible
Contrast, focus rings, and ARIA built into each primitive.
Composable
Primitives compose into patterns — no one-off components.
Foundations
Semantic tokens via @theme. Reference by name — never raw hex. Each token generates bg-*, text-*, border-*, and ring-* utilities.
Surfaces
Canvas
canvas
Surface
surface
Surface Subtle
surface-subtle
Sidebar
sidebar
Text
Ink
ink
Ink Muted
ink-muted
Ink Subtle
ink-subtle
Ink Disabled
ink-disabled
Borders
Line
line
Line Strong
line-strong
Brand
Primary
primary
Accent
accent
Danger
danger
Status
Draft
draft
Pending
pending
Approved
approved
Scheduled
scheduled
Overdue
overdue
Foundations
Geist Sans throughout. text-sm (14 px) is the default body size. Use font-mono tabular-nums for amounts and invoice numbers.
Foundations
Named tokens for consistency. rounded-lg (0.5 rem) is the default for cards and controls. rounded-full for avatars.
xs
sm
md
lg
xl
2xl
full
Foundations
Inline SVGs at 16 × 16 using currentColor. No icon library. Each lives inside the component that uses it; aria-hidden on all decorative instances.
Search
SearchInput
CheckCircle
Toast success
InfoCircle
Toast info
Warning
Toast warning
XCircle
Toast error
X
Dismiss / clear
Spinner
Button loading
ChevronDown
Select
ChevronLeft
Back navigation
Plus
Add line item
Check
Upload success
Invoice preview
Menu
Mobile nav toggle
Grid
Dashboard nav
Receipt
Bills nav
StatusDot
Badge
UploadCloud
Invoice upload
Components
Five variants mapped to intent hierarchy. Accent is the highest-emphasis action — at most one per view. Ghost and secondary support secondary actions without competing for attention.
Variants
Choose based on the action's importance relative to others on the same surface.
All variants — medium size
Sizes
sm for dense toolbars, md default, lg for prominent standalone CTAs.
Primary variant
Secondary variant
States
Loading disables the button and shows a spinner. Click to trigger a 2 s demo.
Loading — click to trigger
Disabled
In context — page toolbar
Accent for the page CTA. Never two accent buttons on the same surface.
Bills
Components
Read-only status indicators for the bill lifecycle. Each variant maps to a semantic foreground/background color pair. Badges never trigger actions.
All variants
Inline with content
Badges sit next to vendor names in list rows.
Components
Groups related content on bg-surface with a subtle border and shadow. Divide internal sections with border-line, not padding alone.
Metric cards
Dashboard KPIs. Large tabular number for the primary value; badge or subtext for context.
To Approve
7
Due This Week
$48,250
5 bills
Paid This Month
$128,450
14 bills
Bill detail card
Header, line-item table, total footer, and action row sectioned inside a single Card with overflow-hidden.
Stripe, Inc.
INV-2025-0834 · Due Jan 15, 2026
| Description | Qty | Amount |
|---|---|---|
| Software infrastructure | 1 | $9,800 |
| API usage fees | 1 | $1,200 |
| Support & maintenance | 1 | $500 |
Total
$11,500
Components
Initials avatar with a color derived from the vendor name hash. Five palettes — deterministic and stable across sessions.
Color palette
Five palettes distributed by name hash. Same name always maps to the same color.
Sizes
Numeric size prop in pixels. Font size scales automatically.
28
36
44
56
In context — list row
Avatar + vendor name is the standard lead for bill list rows.
Components
A composable menu component for contextual actions and single-select controls. Uses a render-prop trigger so ARIA attributes live on the actual button, and fixed positioning to escape overflow:hidden ancestors.
Items
Selected item (checkmark), icons, separator, and disabled state.
Arrow keys navigate · Enter / Space selects · Escape closes
With icon trigger · right-aligned
User-icon trigger and right-edge alignment — the role-switcher pattern.
In context — header
Right-aligned menu anchored to a secondary button. The menu escapes the header's overflow container via fixed positioning.
Payables
Anatomy
Pass items as a flat array. Separators, icons, selected state, and disabled are all item-level flags.
<Dropdown
trigger={({ open, triggerProps }) => (
<Button {...triggerProps} variant="secondary" size="sm">
Open <ChevronDownIcon />
</Button>
)}
items={[
{ id: 'dashboard', label: 'Dashboard', icon: <GridIcon />, selected: true },
{ id: 'bills', label: 'Bills', icon: <ReceiptIcon /> },
{ type: 'separator' },
{ id: 'archive', label: 'Archive', disabled: true },
]}
onSelect={id => navigate(id)}
align="right" // 'left' | 'right' (default: 'left')
/>Components
Hover/focus label for icon-only buttons and anything that needs a short hint without permanent on-screen text. Shows after a brief delay, hides instantly, and positions itself from the trigger's bounding rect — same approach as Dropdown — so it escapes overflow:hidden ancestors.
Sides
Hover or focus each button. Defaults to top.
In context — icon-only trigger
The most common use: a bare icon with no visible label.
Anatomy
Wraps its child — no ref-forwarding required, so any trigger element works.
<Tooltip content="Calculated from the vendor's tax profile." side="top">
<button aria-label="More info">
<InfoCircleIcon className="size-3.5" />
</button>
</Tooltip>Components
Controlled input with built-in search icon and clear button. For table filtering — not form submissions.
Default
Full-width within its container.
In context — toolbar
Constrained width alongside page actions.
Bills
Forms
Input, Select, and Textarea share consistent height, focus rings, and error states. Compose with FormField for accessible labeling.
Input
Single-line text entry.
Default
Filled
Error
Disabled
Select
Native select styled to match Input.
Default
Filled
Error
Disabled
Textarea
Multi-line text. Inherits all Input states.
Default
Filled
Error
Disabled
Label
Always renders as a <label>. The required asterisk is visual only — pair with validation logic.
Forms
Composes Label + description + error around any control. Prefer FormField over raw Label + Input — it wires htmlFor and announces errors to screen readers.
Composition patterns
Accepts any form control as its child.
Printed at the top of the invoice.
Please enter a valid amount.
Used for reporting.
Optional context for the reviewer.
In context — bill create form
The Invoice details section from the Create Bill screen.
As it appears on the document.
Please enter a valid amount.
For budget allocation.
Optional. Visible to approvers during review.
Feedback
Non-blocking notifications that auto-dismiss after 4.5 s. Wired via ToastProvider in the root layout — survives client-side navigations.
Variants — click to trigger
Four variants mapping to semantic status colors. Toasts stack and animate with Framer Motion.
Toasts auto-dismiss after 4.5 s. Click × to dismiss early.
API
import { useToast } from '@/components/ui/toaster'
import {
SearchIcon,
CheckCircleIcon,
InfoCircleIcon,
WarningTriangleIcon,
XCircleIcon,
XIcon,
SpinnerIcon,
ChevronDownIcon,
ChevronLeftIcon,
PlusIcon,
CheckIcon,
PdfIcon,
MenuIcon,
GridIcon,
ReceiptIcon,
StatusDotIcon,
UploadCloudIcon,
} from '@/components/icons'
const { toast } = useToast()
toast({
variant: 'success', // success | info | warning | error
title: 'Bill created',
description: 'Saved as a draft.', // optional
})When to use each variant
Feedback
Persistent inline banner for page-level status that needs to stay visible until its underlying condition changes — unlike Toast, it doesn't auto-dismiss. Reuses Toast's warning/error color and icon pairing under the names warning/danger.
Variants
Content is free-form — pass whatever the situation needs (a label plus a link, a label plus a comment, etc.).
Line item amounts don't match the attached invoice.
API
import { Alert } from '@/components/ui/alert'
<Alert variant="warning"> {/* warning | danger */}
<span className="font-medium">Changes requested</span>
<p className="mt-0.5 text-ink-muted">{comment}</p>
</Alert>When to use each variant
Motion
All animations use shared tokens from lib/motion.ts. CSS transitions and Framer Motion share the same duration and easing vocabulary.
Principles
Duration scale
From lib/motion.ts — duration.*
Variant presets
From lib/motion.ts — variants.*
Usage
import { variants, transition } from '@/lib/motion'
import { motion, AnimatePresence } from 'framer-motion'
// Standard enter / exit
<AnimatePresence>
<motion.div
initial="hidden"
animate="visible"
exit="exit"
variants={variants.slideDown}
>
…
</motion.div>
</AnimatePresence>
// Custom transition override
<motion.div transition={transition.slow} variants={variants.fadeIn}>
…
</motion.div>Accessibility
All components target WCAG 2.1 AA as a baseline. The items below are enforced at the primitive level — not left to individual implementations.
Requirements by category
Patterns
Five-column data table: vendor (with avatar), invoice number, amount, due date, status. Rows hover to bg-surface-subtle.
Recent Bills
| Vendor | Invoice | Amount | Due | Status |
|---|---|---|---|---|
Stripe, Inc. | INV-0834 | $11,500 | Jan 15, 2026 | Pending Approval |
Amazon Web Services | INV-0833 | $8,240 | Jan 20, 2026 | Approved |
Figma | INV-0832 | $6,000 | Dec 31, 2025 | Overdue |
Vercel | INV-0831 | $2,400 | Jan 30, 2026 | Scheduled |
Notion | INV-0830 | $1,200 | Jan 10, 2026 | Paid |