Purpose: Engineers can implement design without clarification spikes.
Audience: Frontend squad, engineering leads.
Timeline: 1-2 sprints for full visual consistency implementation.
Before starting implementation:
- Design specs reviewed: DESIGN_SPEC.md read and questions answered
- Component specs understood: COMPONENT_STATES.md reviewed
- Tokens available:
src/design-tokens.cssimported into main layout - Test strategy agreed: TESTING_CHECKLIST.md signed by QA
- Figma/prototype link: Link to final design (if available) captured in issue
- Known limitations listed: Deferrals acknowledged (see DESIGN_SPEC.md Β§ 12.1)
- Team capacity confirmed: Estimate for implementation
Create /src/design-tokens/:
src/
βββ design-tokens/
β βββ index.css # Main token export (import this)
β βββ colors.css # Color variables (light/dark)
β βββ typography.css # Font definitions
β βββ spacing.css # Space scale
β βββ animations.css # Keyframes & transitions
β βββ breakpoints.css # Responsive utilities
β βββ README.md # Token usage guide
βββ components/
β βββ Button.tsx # Refactored with tokens
β βββ Input.tsx # New component (if not exists)
β βββ Modal.tsx # Refactored
β βββ Navigation.tsx # Refactored
β βββ ...
βββ styles/
β βββ globals.css # Global resets, :root, @media queries
β βββ accessibility.css # Focus styles, reduced-motion
βββ App.tsx # Updated to import design-tokens
βββ main.tsx
File: src/design-tokens.css (or /src/design-tokens/index.css)
Content: Copy from DESIGN_SPEC.md Β§ 3.2 or use the prepared src/design-tokens.css file.
Import in App.tsx or main.tsx:
// src/main.tsx
import './design-tokens.css'; // Must be imported FIRST
import './index.css';
import App from './App.tsx';Minimal reset (add to src/index.css):
@import './design-tokens.css';
/* Global resets */
* {
box-sizing: border-box;
}
html, body, #root {
margin: 0;
padding: 0;
height: 100%;
}
body {
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font: var(--font-body-md);
font-family: var(--font-family-base);
transition: background-color var(--transition-base),
color var(--transition-base);
}
/* Reset link styles */
a {
color: var(--color-accent-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
/* Accessibility: Focus styles globally */
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: var(--focus-outline);
outline-offset: var(--focus-outline-offset);
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}File to update: src/components/Button.tsx (create new or update existing)
Migration steps:
-
Remove inline styles (if using React.CSSProperties)
-
Add CSS module or scoped styles:
// src/components/Button.tsx import React from 'react'; import './Button.css'; // NEW: CSS module or global interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { variant?: 'primary' | 'secondary' | 'tertiary'; size?: 'sm' | 'md' | 'lg'; loading?: boolean; children: React.ReactNode; } export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ variant = 'primary', size = 'md', loading = false, disabled, children, className, ...props }, ref) => { const classNames = [ 'button', `button--${variant}`, `button--${size}`, loading ? 'button--loading' : '', className, ].filter(Boolean).join(' '); return ( <button ref={ref} className={classNames} disabled={disabled || loading} aria-busy={loading} {...props} > {loading && <span className="spinner" aria-hidden="true" />} {children} </button> ); } );
-
Create Button.css (use design tokens):
/* src/components/Button.css */ .button { display: inline-flex; align-items: center; gap: var(--space-sm); padding: var(--space-md) var(--space-lg); font: var(--font-label-lg); border: none; border-radius: var(--radius-md); cursor: pointer; transition: all var(--transition-base); outline: none; } .button:hover:not(:disabled) { background-color: var(--color-accent-primary-dark); box-shadow: var(--shadow-lg); } .button:focus-visible { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); } .button:active:not(:disabled) { transform: translateY(1px); } .button:disabled { background-color: var(--color-text-tertiary); color: var(--color-text-muted); opacity: 0.4; cursor: not-allowed; } /* Variants */ .button--primary { background-color: var(--color-accent-primary); color: var(--color-text-inverse); box-shadow: var(--shadow-accent-primary); } .button--secondary { background-color: var(--color-surface-raised); color: var(--color-text-primary); border: 1px solid var(--color-border-default); } .button--loading { pointer-events: none; } .spinner { display: inline-block; width: 16px; height: 16px; border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; }
-
Update all button usages:
// OLD inline styles (remove) <button style={{ background: 'var(--accent)', ... }}>Create</button> // NEW component (prefer this) <Button variant="primary">Create</Button>
-
Testing: Verify button states (default, hover, focus, disabled, loading) match COMPONENT_STATES.md
File: src/components/Input.tsx (create new)
Steps (similar to Button):
- Create component with label, error, helper text support
- Merge associated label tag (not floating label yet)
- Add aria-invalid, aria-required, aria-describedby
- CSS: Use design tokens for colors, spacing, transitions
- Update all form fields in:
CreateStreamModal.tsxConnectWalletModal.tsx- Any other forms
Code example (see COMPONENT_STATES.md Β§ Input Component)
File: src/components/Navigation.tsx or Sidebar.tsx + AppNavbar.tsx
Steps:
-
Unify navigation items:
- Update Sidebar nav items to use new NavItem component
- Apply consistent styles from COMPONENT_STATES.md Β§ Navigation Item
-
Add aria-current="page":
<Link to="/app" className={`nav-item ${isActive ? 'nav-item--active' : ''}`} aria-current={isActive ? 'page' : undefined} > Dashboard </Link>
-
Update CSS for nav items (see COMPONENT_STATES.md)
-
Mobile navigation: Ensure hamburger menu works at β€768px with focus trap
File: src/components/Modal.tsx (create base component)
Steps:
-
Create base Modal component:
- Handles backdrop, dialog structure
- Manages focus trap (Tab, Shift+Tab)
- Manages Escape key
- Returns focus to trigger button on close
-
Update
CreateStreamModal.tsx:// OLD: inline styles + manual backdrop // NEW: <Modal isOpen={} onClose={} title=""> structure <Modal isOpen={isOpen} onClose={onClose} title="Create Stream" > {/* Form fields */} </Modal>
-
Similarly update
ConnectWalletModal.tsx, any other modals -
CSS: Use design tokens for backdrop, shadow, entrance/exit animations
File: src/components/EmptyState.tsx (create new)
Steps:
-
Convert existing empty state components to unified EmptyState
-
Props:
interface EmptyStateProps { icon: React.ReactNode; title: string; description: string; ctaLabel: string; onCta: () => void; }
-
Update usage in Dashboard, Recipient pages
-
CSS: Center icon + text; 80Γ80px border icon; CTA button
- Use design tokens for spacing, colors
File: src/components/Skeleton.tsx (create new)
Purpose: Reusable loading placeholder
Code (see COMPONENT_STATES.md Β§ Skeleton Loading Component)
Usage:
// In TreasuryOverviewLoading.tsx
<Skeleton height="24px" count={3} />File: src/components/StatusBadge.tsx (create new)
Purpose: Reusable status indicator
Code (see COMPONENT_STATES.md Β§ Status Badge Component)
Usage:
<StatusBadge status="active" label="Active" />File: src/components/Layout.tsx
Requirements:
- Sidebar: Fixed left panel (244px width desktop, hidden mobile)
- Header: Top bar with logo, breadcrumb, theme toggle, wallet info
- Main content: Flex-grow area with page content
- Breakpoints:
- Desktop (β₯1024px): Sidebar visible + main side-by-side
- Tablet (768-1024px): Sidebar collapsible
- Mobile (<768px): Sidebar hidden β hamburger drawer
CSS structure:
.app-layout {
display: grid;
grid-template-rows: auto 1fr;
min-height: 100vh;
background-color: var(--color-bg-primary);
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-lg);
border-bottom: 1px solid var(--color-border-default);
height: 64px;
}
.app-body {
display: grid;
grid-template-columns: 244px 1fr;
gap: 0;
overflow: hidden;
}
.app-sidebar {
background-color: var(--color-surface-default);
border-right: 1px solid var(--color-border-default);
width: 244px;
overflow-y: auto;
padding: var(--space-lg);
}
.app-main {
padding: var(--space-2xl);
overflow-y: auto;
}
/* Mobile */
@media (max-width: 768px) {
.app-body {
grid-template-columns: 1fr;
}
.app-sidebar {
position: fixed;
left: -244px;
top: 64px;
height: calc(100vh - 64px);
z-index: 40;
transition: left var(--transition-base);
}
.app-sidebar.open {
left: 0;
}
}File: src/pages/Landing.tsx and related
Requirements:
- Hero section: Full-width radial gradient, heading, subheading, 2 CTAs
- Trust section: 3-column use case cards (1 column mobile)
- Footer: Links, copyright
- Theme-aware: Light/dark gradient backgrounds
- Typography: Use design tokens (heading 1/2, body, labels)
Already mostly correct - ensure:
- Colors use design tokens (no hardcoded #hex)
- CTA buttons use unified Button component
- Responsive at 320px, 640px, 1024px
- No hardcoded spacing (use design tokens)
Change:
// ADD at top
import './design-tokens.css'; // Import tokens first
// ADD theme manager
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
const saved = localStorage.getItem('theme');
if (saved) return saved as 'light' | 'dark';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}, [theme]);
// JSX: theme toggle button
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
className="theme-toggle"
aria-label="Toggle theme"
>
{theme === 'light' ? 'π' : 'βοΈ'}
</button>Changes:
- Remove duplicate nav code (currently has mixed code)
- Consolidate Header + Sidebar into unified Layout
- Add responsive burger menu for mobile
- Import refactored navigation components
Changes:
- Replace inline styles with
var(--color-*),var(--space-*) - Use Button component instead of inline buttons
- Use Skeleton component for loading state
- Use EmptyState component for no-data state
- Use StatusBadge in tables/lists
- Import design-tokens.css in main.tsx
- Create Button component + update all button usages
- Create Input component
- Update global styles (index.css, accessibility resets)
- Refactor Layout.tsx (consolidate, remove duplicates)
- Add responsive hamburger for mobile
- Update Navigation items with consistent styling
- Add theme management (light/dark toggle)
- Create base Modal component
- Update CreateStreamModal to use Modal
- Update ConnectWalletModal to use Modal
- Test focus trap and keyboard navigation
- Update Dashboard page (metrics, empty state, loading)
- Update Streams page
- Update Recipient page
- Create Skeleton loading component
- Create StatusBadge component
- Audit all components for ARIA attributes
- Test keyboard navigation (Tab, Escape, etc.)
- Test screen reader (VoiceOver/NVDA)
- Verify color contrast (WAVE)
- Test responsive breakpoints (320px, 768px, 1024px)
- Test theme toggle
- Test reduced-motion preference
- Run TESTING_CHECKLIST.md full suite
- Lighthouse Accessibility score β₯90
- No visual regressions from spec
- Design sign-off completed
β All button states render correctly (default, hover, focus, active, disabled, loading)
β Focus ring visible, cyan, 2px, 2px offset
β Keyboard: Tab focuses, Enter/Space activates
β ARIA: aria-busy on loading, aria-disabled on disabled
β Uses design tokens (no hardcoded colors/sizes)
β Passes WAVE scan (no contrast issues)
β Responsive: Works on mobile (44px+ touch target)
β All input states correct (default, hover, focus, error, disabled, success)
β Error state: red border, red text, helper message
β Focus ring visible on focus
β Keyboard: Tab focuses, Type enters text, Tab moves to next
β ARIA: aria-label or <label>, aria-invalid, aria-required, aria-describedby
β Passes WAVE contrast scan
β Helper text displayed below input
β Backdrop visible with correct color
β Dialog centered, correct shadow
β Close button functional (X icon)
β Keyboard: Tab traps within modal, Escape closes, focus returns
β ARIA: role="dialog", aria-modal, aria-labelledby
β Animation: Smooth entrance (scale 0.95β1), smooth exit
β prefers-reduced-motion: Animation instant
β Sidebar: 244px desktop, hidden mobile with hamburger
β Header: 64px height, logo + nav + theme + wallet
β Main: Full-width padding, colored background
β Responsive: Works 320px, 768px, 1024px breakpoints
β No horizontal scroll at 320px
β Hamburger menu: Opens/closes smoothly, focus trap
β Theme toggle: Light/dark persists in localStorage
β Empty state: Icon + title + CTA visible when no streams
β Metrics: 3 cards showing when connected
β Loading: Skeleton shows for β₯2s on first load
β Error state: Error message displayed clearly
β CTA button: "+ Create Stream" prominent, functional
β Responsive: Metrics stack on mobile
When reviewing implementation PRs:
- Tokens used: All colors, spacing, fonts from variables (grep for #hex, hardcoded px)
- ARIA complete: aria-label, aria-invalid, aria-required, aria-busy, aria-labelledby present
- Keyboard accessible: Tab order logical, focus visible, Escape for modals
- Screen reader tested: Labels, alerts, status announced correctly
- Focus ring: 2px cyan ring, 2px offset, all interactive elements
- Responsive: Mobile (320px), tablet (768px), desktop (1024px) tested
- Color contrast: WAVE scan shows no errors
- States complete: Default, hover, focus, active, disabled documented
- Accessibility attributes:
aria-*attributes correctly applied - No layout shift: Skeletons prevent Cumulative Layout Shift
- Performance: No unnecessary re-renders, smooth animations
If critical issues arise during implementation:
- Visual bugs: Revert affected component file to previous commit
- Accessibility issues: Disable feature in environment variable until fixed
- Performance regression: Profile with DevTools; identify heavy component
- Browser incompatibility: Feature flag for older browsers
After implementation:
- Design tokens README: /src/design-tokens/README.md documents token usage
- Component documentation: Each component has JSDoc comments
- DESIGN_SPEC.md: Kept up-to-date as implementation proceeds
- Team training: 30-min sync explaining token system, component patterns
- Design decisions logged: Rationale for any deviations from spec captured in comments or issue
After deployment to production:
- Accessibility audit: Third-party audit or WAVE + Axe monthly scan
- User feedback: Monitor support tickets for UX issues
- Performance metrics: Lighthouse Accessibility score, Core Web Vitals
- Bug reports: Triage and prioritize accessibility/contrast issues
- Continuous improvement: Backlog improvements identified during QA
Q: Which CSS-in-JS library should I use?
A: Prefer CSS files + Tailwind over runtime CSS-in-JS. Tokens work best in static CSS.
Q: Can I use Styled Components?
A: Not recommended. CSS variables work better with CSS files or Tailwind. If needed, document token usage in JSDoc.
Q: How do I handle component variants in CSS?
A: Use BEM-style class names (.button--primary, .button--secondary). See COMPONENT_STATES.md for patterns.
Q: Should I migrate to Tailwind completely?
A: Not required. Mix Tailwind utilities with design tokens. Consistency more important than tool choice.
Q: How do I test theme toggle?
A: Programmatically: document.documentElement.setAttribute("data-theme", "dark"). Verify computed colors change.
Q: What if a component needs a custom color?
A: Add to design-tokens.css as new semantic token. Document rationale. Avoid one-off hardcoded colors.
Q: How do I handle loading states without skeleton?
A: Use spinner or opacity fade. Skeleton preferred to prevent layout shift (Cumulative Layout Shift metric).
Q: Can I use CSS Modules?
A: Yes! Create Button.module.css, import as import styles from './Button.module.css', use className={styles.button}.
Project complete when:
β All component states match COMPONENT_STATES.md
β 100% of colors use CSS variables (zero hardcoded #hex)
β 100% of spacing uses design tokens (zero random px)
β All interactive elements have focus ring (cyan, 2px, 2px offset)
β WAVE scan: 0 contrast errors, 0 missing alt text, 0 missing labels
β Keyboard navigation: Tab, Shift+Tab, Enter, Space, Escape all work
β Screen reader: Dashboard readable with VoiceOver/NVDA
β Responsive: No horizontal scroll at 320px viewport
β Theme toggle: Light/dark works seamlessly, persists
β Lighthouse Accessibility: Score β₯90/100
β Visual regression: All pages match design spec screenshots
β Design sign-off: PM + Design + Engineering confirm completion
Document Version: 1.0
Last Updated: March 30, 2026
Status: β
Ready for Implementation Kickoff
# Start development
npm run dev
# Run Lighthouse audit
# β DevTools β Lighthouse β Generate Report β Accessibility
# Check CSS coverage
grep -r "#[0-9A-Fa-f]\{6\}" src/components/
# List all .tsx component files
find src/components -name "*.tsx" | sort
# Check for hardcoded px values (may have false positives)
grep -r "[0-9]\+px" src/components/ | grep -v "var(" | head -20Questions? Open an issue with tag [implementation-help] or reach out to the design lead.