Internal · v0.1.0

Payables
Design System

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

Color

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

Aa

Draft

draft

Aa

Pending

pending

Aa

Approved

approved

Aa

Scheduled

scheduled

Aa

Overdue

overdue

Foundations

Typography

Geist Sans throughout. text-sm (14 px) is the default body size. Use font-mono tabular-nums for amounts and invoice numbers.

displayDisplay
2xlHeading 2XL
xlHeading XL
lgHeading LG
baseBase — default paragraph size (16 px)
smSmall — default UI label size (14 px)
xsExtra small — captions and metadata (12 px)
2xsTwo extra small — overlines (11 px)

Foundations

Radius

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

Icons

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

Pdf

Invoice preview

Menu

Mobile nav toggle

Grid

Dashboard nav

Receipt

Bills nav

StatusDot

Badge

UploadCloud

Invoice upload

Components

Button

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

Badge

Read-only status indicators for the bill lifecycle. Each variant maps to a semantic foreground/background color pair. Badges never trigger actions.

All variants

DraftPending ApprovalApprovedScheduledPaidOverdueDefault

Inline with content

Badges sit next to vendor names in list rows.

Stripe, Inc.Pending Approval
Amazon Web ServicesApproved
FigmaOverdue
VercelScheduled
NotionPaid

Components

Card

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

3 overdue

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

Pending Approval
DescriptionQtyAmount
Software infrastructure1$9,800
API usage fees1$1,200
Support & maintenance1$500

Total

$11,500

Components

VendorAvatar

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.

Stripe, Inc.
Amazon Web Services
Figma
Vercel
Notion

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.

Stripe, Inc.INV-0834Pending Approval$11,500
Amazon Web ServicesINV-0833Approved$8,240
FigmaINV-0832Overdue$6,000

Components

Dropdown

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

Tooltip

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.

Amount includes tax

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

SearchInput

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

Inputs

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

FormField

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.

Used for reporting.

Optional context for the reviewer.

In context — bill create form

The Invoice details section from the Create Bill screen.

Invoice details

As it appears on the document.

For budget allocation.

Optional. Visible to approvers during review.

Feedback

Toast

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

successCompleted actions — bill created, approved, updated.
infoNeutral updates — sync started, data loading.
warningNon-blocking caution — payment may be delayed.
errorFailures — constraint violations, save errors.

Feedback

Alert

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.).

Changes requested

Line item amounts don't match the attached invoice.

Required fields missingEdit bill

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

warningNeeds attention but isn’t blocking — e.g. changes requested by a manager.
dangerBlocks progress until resolved — e.g. required fields missing before submit.

Motion

Animation

All animations use shared tokens from lib/motion.ts. CSS transitions and Framer Motion share the same duration and easing vocabulary.

Principles

Subtle over theatricalTransitions are 50–150 ms — just enough to orient, never to entertain.
PurposefulEvery animation communicates a state change. Purely decorative motion is avoided.
Consistent vocabularyduration.*, ease.*, and variants.* are the only sources of truth. No one-off values.

Duration scale

From lib/motion.ts — duration.*

instant50 msState swap — no perceptible motion
fast100 msHover states, icon swaps
normal150 msStandard UI transition (default)
slow250 msDeliberate, multi-property transitions
slower400 msPage-level or cinematic moments

Variant presets

From lib/motion.ts — variants.*

fadeInOverlays, tooltips — opacity only
slideUpDropdowns, cards entering from below
slideDownToasts, notifications entering from above
scaleInModals, context menus — scale + fade
listItemStaggered list rows (staggerChildren: 0.05)

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

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

Keyboard navigationAll interactive elements reachable via Tab. Buttons and links activate with Enter or Space. No keyboard traps — Escape closes overlays.
Focus-visibleAll focusable elements show a 2 px ring on :focus-visible (not :focus) using the primary color with ring-offset. No ring on mouse click.
WCAG AA contrastink on surface exceeds 7:1 (AAA). ink-muted on surface exceeds 4.5:1 (AA). All status foreground/background pairs pass AA. Danger meets AA for text and icons.
Semantic HTML<button> for every action — never <div onClick>. <label> wired via FormField. <table> for tabular data. <nav> with aria-label for navigation landmarks.
ARIAaria-label on all icon-only controls. aria-live="polite" on the toast container. role="status" on each toast. aria-hidden="true" on decorative SVG icons.

Patterns

Bill List

Five-column data table: vendor (with avatar), invoice number, amount, due date, status. Rows hover to bg-surface-subtle.

Recent Bills

VendorInvoiceAmountDueStatus
Stripe, Inc.
INV-0834$11,500Jan 15, 2026Pending Approval
Amazon Web Services
INV-0833$8,240Jan 20, 2026Approved
Figma
INV-0832$6,000Dec 31, 2025Overdue
Vercel
INV-0831$2,400Jan 30, 2026Scheduled
Notion
INV-0830$1,200Jan 10, 2026Paid