What is a Design System and why build it first?
A Design System is a single source of truth for all visual and interaction decisions in an app — colors, typography, spacing, components, states, accessibility rules.
The reason to build it before any screen is simple: if you change the primary color after building 7 screens, you're updating it in 40 places. If you change it in the DS, it propagates everywhere automatically.
For HandyFEM I built the DS in two layers:
Layer 1 — Tokens: CSS variables and Tailwind config values. Colors, spacing, border radius, shadows, transitions. Everything that gets used across components.
Layer 2 — Components: Button, Input, Card, Badge, Avatar. Each one with all variants, all states, and full accessibility spec.
The token decisions: here are some design desicions.
Colors
The palette was already defined from HandyFEM's social media posts and brand materials — teal as primary, violet as accent, with neutrals for backgrounds and borders.
The key decision was dropping the cream background in favor of clean white and light gray. Cream looks warm in isolation but gets muddy next to colored components. White/gray lets the teal and violet breathe.
--color-primary: #4A7C7D; /* teal */
--color-accent: #776AAA; /* violet */
--color-bg-primary: #ffffff;
--color-bg-secondary: #F5F5F5;
--color-border: #E0DDD6; /* neutral gray */
--color-amber: #FCC970; /* ratings only */
Spacing
Base 4px system. Every spacing value is a multiple of 4. This sounds rigid but in practice it makes layouts feel consistent without effort.
Border radius
The interesting decision here: buttons are 8px (rounded), not pill-shaped. The navbar is pill (9999px). This combination — pill navbar + rounded buttons — is what Linear, Vercel and Notion use. It gives sophistication without being generic.
The components
DS-01 — Button
Four variants: primary (teal), secondary (violet outline), ghost (text only), destructive (red outline). Three sizes: large (48px), medium (40px), small (32px for filters and chips).
The non-obvious decisions:
- Loading state blocks double-submit — critical for auth forms
- Destructive always requires an AlertDialog confirmation before firing
-
@media (hover: hover)wrapping on all hover effects — no sticky hover states on touch devices -
prefers-reduced-motionremoves scale animation, keeps color changes
DS-02 — Inputs
Label always above, never floating. Floating labels are visually clever but have serious accessibility edge cases with screen readers and mobile keyboards. Not worth it.
Validation fires onBlur — never in real time while typing. Real-time validation is annoying when you haven't finished the word yet.
The file upload component has drag-and-drop, immediate preview, and opens camera/gallery on mobile. This matters for the professional onboarding — portfolio photos are a core part of the profile.
DS-03 — Professional Cards
Two layouts: horizontal for mobile (photo left, info right), vertical for desktop (photo top, info bottom in a 2-column grid). The border is 0.5px solid #E0DDD6 — neutral gray, not the lavender that was in the first iteration. The lavender competed with the content. Gray doesn't.
The "Verified" badge has a pulsing green dot. Small detail, big signal — it communicates that the profile is active and has been reviewed.
DS-04 — Badges and Chips
Status badges are pill-shaped. Category tags are rounded (6px). This distinction encodes hierarchy — pill for important single states, rounded for informational groups. Same pattern that GitHub and Linear use.
Filter chips show an X icon when active. One pending polish note: the X is too small and slightly misaligned — will fix in code, not in spec.
DS-05 — Avatar
Color assigned by name, not randomly. The first character of the name maps to one of four background colors via charCode % 4. Marta López is always lavender. Sara Ruiz is always teal. Consistent across the whole app, across all devices, forever.
The workflow: spec → visual preview → approve → document
For each component, the process was:
- Define variants, states, and decisions in text
- Generate a live visual preview in Claude Design
- Review and adjust (border color, shape, sizing)
- Lock the decision in
docs/handyfem-specs.md
Having the visual preview before writing code meant design decisions were made consciously, not discovered mid-implementation. The spec document in /docs is now the reference for every component — Claude Code will use it to generate the actual React + Tailwind + shadcn/ui code.
What the spec document looks like
Each component entry in handyfem-specs.md includes:
- All variants with exact hex values
- All states (default, hover, focus, error, disabled, loading)
- Token references (no hardcoded values)
- Accessibility requirements (aria attributes, focus rings, touch targets)
- shadcn/ui implementation notes
- Props interface
This is specs-driven development — write the spec, then write the code. A well-written spec is also the prompt for Claude Code, which means the first generated output is much closer to final.
The interaction details
Four effects, chosen carefully:
Navbar pill with scroll shrink — uses animation-timeline: scroll() to progressively shrink the navbar from 100% to 88% as the user scrolls. Degrades gracefully on Firefox (stays at 100%, still functional).
Hero fade + slide up — opacity: 0 → 1 with translateY(16px → 0) on load. Staggered: title first, subtitle 100ms later, CTAs 200ms later.
Card hover — translateY(-2px) with a teal-tinted shadow. Only on @media (hover: hover) — touch devices don't get stuck hover states.
Scroll-triggered fade — sections enter the viewport with a fade + slide via IntersectionObserver. Implemented as a reusable useScrollReveal hook.
All four respect prefers-reduced-motion.
What's next
The Design System spec is done. Next step: Claude Code — implementing all of this as actual React components in the Next.js project, starting with globals.css, tailwind.config.ts, and the component files one by one.
After that: building the 7 MVP screens using the DS as the foundation.
But first. Let's make a proper plan, by using JIRA to organize all the work!
📚 HandyFEM App Series
🔗 Previous: From Idea to Specs: Planning HandyFEM's Architecture with Claude.ai - Specs Driven Development
🔗 Next: From Specs to Tickets: Automating Jira Setup with Node.js and the Jira API
Building in public. All mistakes included.


Top comments (0)