DESIGN.md
Reactive Resume is a monochrome, content-first design system built for a resume builder used by tens of thousands of people worldwide. The visual identity prioritizes readability and unobtrusiveness — the user's resume content is always the hero, never the chrome around it.
The system defaults to dark mode with a warm near-black backdrop that makes the resume preview "float" as the visual anchor. Light mode is supported as a full alternative. The authenticated app shell (dashboard, builder, settings) uses an entirely achromatic grayscale palette — the sole chromatic exception is destructive red for dangerous actions. The landing page introduces subtle chromatic accents: blue-tinted spotlight gradients on the hero, a multicolor text-mask animation on hover, and social auth provider brand colors (Google blue, LinkedIn blue) on the login page.
The overall aesthetic is a professional tool UI: clean grid lines, subtle borders, generous whitespace, and typography that steps back to let the content shine. Think "VS Code meets Figma" — a productivity workspace, not a marketing site.
One deliberate counterpoint to the serious UI: all resume templates are named after Pokemon (Azurill, Bronzor, Chikorita, Ditgar, Gengar, Pikachu, etc.). This is an intentional brand choice — playful naming for templates injects personality into an otherwise utilitarian interface, making templates feel collectible and memorable rather than generic ("Template 1", "Modern", "Classic").
The palette is rooted in achromatic OKLch values (chroma = 0), producing a pure grayscale scale without warm or cool casts. Colors are defined as CSS custom properties using oklch() and consumed through Tailwind CSS 4 theme tokens. Always prefer CSS variables (e.g., var(--primary)) or Tailwind tokens (e.g., bg-primary) over raw color values. The hex values in this document's YAML front matter are agent-friendly approximations of the canonical OKLch definitions in packages/ui/src/styles/globals.css — use hex only where OKLch is unavailable.
Resume templates have their own independent color system — users pick primary, text, and background colors per resume through a color picker in the builder's Design panel. These template colors are completely separate from the app shell palette.
The entire application uses a single typeface: IBM Plex Sans Variable. This is a humanist sans-serif with an extensive weight range (100–900) and excellent readability at small sizes, both on screen and in PDFs.
The resume content itself uses a separate font system — users choose from 1,000+ Google Fonts for their resume headings and body text, with category-aware fallback stacks including CJK support (Noto Sans SC, PingFang SC, Hiragino Sans GB for sans-serif; Noto Serif SC, Songti SC for serif). Standard PDF fonts (Helvetica, Courier, Times-Roman) are available as offline fallbacks.
Font rendering uses antialiased (grayscale AA) and proportional-nums across the board for clean rendering and properly spaced numerals in dates and phone numbers.
The core builder uses a resizable three-panel layout powered by react-resizable-panels:
react-zoom-pan-pinch. The preview maintains A4 aspect ratio (210:297) with a subtle shadow to simulate a physical page.Panel sizes persist in cookies. On mobile (< 768px), sidebars collapse to 0% width and become toggleable overlays (max 95% width when open). The desktop minimum collapsed width is 48px (icon rail).
Standard sidebar navigation layout using the Sidebar component system. The sidebar contains: logo, resume list link, agent link, settings subnavigation (profile, preferences, authentication, API keys, integrations, danger zone), and a footer with user avatar. Content area shows a responsive grid of resume cards.
Full-width single-column marketing layout:
Mobile detection uses a 768px threshold via MediaQueryList. The layout is optimized for workspace productivity on larger screens, with responsive mobile support that adapts the multi-panel builder into a streamlined single-panel experience. Both desktop and mobile are supported experiences — the builder's three-panel layout leverages desktop space, while mobile surfaces the same editing capabilities through collapsible overlays.
A custom Tailwind token --aspect-page: 210 / 297 enforces A4 paper proportions wherever resume pages are rendered (builder preview, public view, PDF export).
Animations use the Motion library (formerly Framer Motion) and follow a consistent choreography pattern:
Entrance animations use a fade-up reveal: elements start at opacity: 0, y: 20-100 and animate to opacity: 1, y: 0. The hero section uses a larger y-offset (100px) for dramatic effect; subsequent sections use 20px for subtlety.
Timing principles:
index * 0.03–0.1 for per-item stagger.easeOut for entrances (elements decelerate into position). easeInOut for looping/ambient animations.will-change-[transform,opacity] on animated elements and will-change-transform on continuously animated elements.Hover/interaction animations are quick (0.2s) and subtle — small scale bumps (scale: 1.01), slight y-offsets (y: -2), and active:translate-y-px for button press.
Ambient animations loop infinitely with easeInOut — the scroll indicator bounces gently (y: [0, 5, 0] over 1.5s).
Reduced motion: All CSS transitions and animations collapse to 0.01ms duration and single iteration when prefers-reduced-motion: reduce is active. Motion library animations should also respect this preference.
Elevation is handled through background color layering rather than drop shadows:
--background).--card), used for sidebars, panels, and cards.backdrop-blur-xs at 0.5px or backdrop-blur-2xl at 40px) with backdrop-saturate-150 for modal overlays, creating a frosted-glass effect over the workspace.The resume preview page uses a subtle drop shadow to simulate a physical sheet of paper floating above the dark artboard — one of the few places actual shadows appear.
Border radius follows a multiplicative scale from a single --radius base of 0.3rem:
| Token | Value | Usage |
|---|---|---|
sm | 0.18rem (≈3px) | Small badges, inline chips |
md | 0.24rem (≈4px) | XS/SM buttons, compact elements |
lg | 0.3rem (≈5px) | Default buttons, cards, inputs |
xl | 0.42rem (≈7px) | Larger cards, modal corners |
2xl | 0.54rem (≈9px) | Dialog containers |
3xl | 0.66rem (≈11px) | Large panels |
4xl | 0.78rem (≈12px) | Full-page modals |
The radius scale is deliberately tight — the largest value (0.78rem) is still quite subtle. This avoids the "rounded everything" aesthetic and keeps the UI feeling precise and tool-like. Interactive elements consistently use rounded-lg as the default.
Six variants, all sharing rounded-lg corners, font-medium, text-sm, and a 1px translate-y on active press (except when the button opens a popup):
Size scale: xs (28px), sm (32px), default (36px), lg (40px), plus icon variants at each size for square icon-only buttons.
White/dark surface with foreground text. Composed of CardHeader, CardTitle, CardDescription, CardContent, CardFooter, and CardAction slots. Default vertical padding is py-4 (compact: py-3).
Built on TanStack Form with Zod validation. Composed of FormItem, FormLabel, FormControl, FormMessage, and FormDescription. Validation errors only appear after field touch. Invalid fields get a red destructive border with a ring.
Centralized dialog manager with 40+ dialog types, all rendered via pattern matching (ts-pattern). Dialogs support before-close validation, form blocking for unsaved changes, and confirmation prompts. Used for all CRUD operations on resume sections, settings changes, and import/export flows.
Triggered by Cmd+K / Ctrl+K. Built on cmdk with fuzzy search via Fuse.js. Multi-page navigation (resumes, settings, preferences) with back navigation via Backspace. Screen-reader accessible with sr-only headings.
Powered by Sonner, positioned bottom-right with rich colors. Used for auto-save feedback, form submission status, error reporting, and donation prompts. Loading toasts are used during async operations (PDF generation, resume creation) with dismiss-on-complete.
Powered by @dnd-kit with PointerSensor and KeyboardSensor. Used in chip inputs (skill tags, URL lists) and page layout management (section ordering across resume pages). Smooth animations via Motion library.
The app supports 40+ locales including RTL languages (Arabic, Hebrew, Persian, Urdu, Uyghur, Yiddish). i18n is not an afterthought — it shapes layout decisions:
Direction: The <html> element receives dir="rtl" or dir="ltr" based on the active locale, detected via isRTL() which checks the language prefix against a known RTL set. All layout mirroring flows from this single attribute.
Logical properties: Use CSS logical properties (ps-, pe-, ms-, me-, inline-start, inline-end, inset-s-, inset-e-) instead of physical (pl-, pr-, ml-, mr-, left, right). Button components already use has-data-[icon=inline-start]:ps-2 and has-data-[icon=inline-end]:pe-2 patterns. This ensures correct spacing in both LTR and RTL layouts without separate stylesheets.
Variable-length text: Translations can be 30–50% longer than English (German, Finnish) or significantly shorter (CJK). UI elements should accommodate variable text length — avoid fixed widths on buttons and labels. Use whitespace-nowrap only where truncation is acceptable, and prefer min-w-0 with truncate over fixed-width containers.
Icons: Directional icons (arrows, chevrons, progress indicators) should mirror in RTL contexts. Phosphor Icons provides mirrored variants for directional icons. Non-directional icons (settings gear, checkmark, delete) do not mirror.
Strings: All user-facing strings use Lingui macros (t, msg, <Trans>) — never hardcode English text in components. Translation files are .po format under /locale/.
text-sm (14px) as the base text size. The UI is information-dense — form fields, section labels, metadata — and needs to be scannable without feeling cramped.rounded-lg (0.3rem) as the default. The tool should feel precise, not playful.prefers-reduced-motion: reduce is active.size-4 (16px) default. Icons should be functional labels, not decorative.oklch(1 0 0 / 10%) blends naturally with any surface rather than introducing a distinct gray band.rounded-xl on standard components. Large pills and full-round shapes conflict with the precision-tool aesthetic.data-slot attribute on components. It's used for styling hooks and accessibility selectors throughout the component library.ps, pe, ms, me) instead of physical (pl, pr, ml, mr).