Magpie.

Design System · v1.0

For engineers building Magpie

The Magpie
design system.

Editorial, restrained, audit-ready. Sage & cream palette with sharp typography and zero rounded corners on landing surfaces. This page is the source of truth — if a component disagrees with it, fix the component.

01 — Foundation

Design principles

Editorial, not corporate

We read like a magazine for regulators. Sharp grids, mono eyebrows, generous whitespace. No drop shadows, no gradients, no purple.

Sharp corners, sharp ideas

Landing and marketing surfaces use 0px radius. Product chrome may use the semantic --radius token (10px) for inputs and cards.

Tokens over hex

Never hard-code colors in components. Use CSS variables (--landing-*, --primary, --foreground) or their Tailwind aliases.

Mono for metadata

Eyebrows, labels, timestamps, IDs use uppercase mono at 10–11px with letter-spacing 0.25em.

Motion is restraint

Hover lifts of -translate-y-0.5, color/opacity transitions only. No bouncy springs. Honor prefers-reduced-motion.

Accessibility is non-negotiable

AA contrast minimum, visible focus rings, semantic HTML, alt text on every img, keyboard-reachable everywhere.

02 — Tokens

Color

Landing palette · Sage & Cream

Used across /, marketing pages, the CMS, and the v2 app shell. Reference via bg-[var(--landing-bg)] or text-[var(--landing-fg)].

Background

--landing-bg

#f5f0e8

Surface

--landing-surface

#e8efe2

Accent

--landing-accent

#dce5d4

Foreground

--landing-fg

#2d3a2a

Foreground soft

--landing-fg-soft

#3a4a37

Semantic tokens (shadcn)

Used by shadcn/ui components. Auto-themed via :root and .dark. Available as Tailwind utilities: bg-primary, text-foreground, etc.

background

--background

foreground

--foreground

card

--card

primary

--primary

secondary

--secondary

muted

--muted

accent

--accent

destructive

--destructive

border

--border

ring

--ring

Rule: never write hex in components.

className="bg-[#2d3a2a]"   ✅ className="bg-[var(--landing-fg)]" or bg-foreground

03 — Tokens

Typography

Display

Space Grotesk

Headings, titles, buttons, brand wordmarks. Weights: 400 (regular), 500 (medium), 700 (bold). Apply via style={{ fontFamily: "'Space Grotesk', sans-serif" }}.

Body

DM Sans

Paragraphs, descriptions, table data. The default for everything that isn't a heading. Inherits from --font-sans via Inter fallback if DM Sans not loaded.

Mono

JetBrains Mono

Eyebrows, IDs, timestamps, code. Always uppercase with tracking-[0.25em] at 10–11px when used as label.

Type scale

Display 60px / 0.98

H1 — 36px / tight

H2 — 24px / tight

H3 — 18px / medium

Body — 16px / relaxed leading

Small — 14px / 80% opacity for secondary

EYEBROW — 11px MONO

04 — Layout

Spacing & grid

Container

Page wrapper: mx-auto max-w-7xl px-6 lg:px-12. Section vertical rhythm: py-16 lg:py-20 with a top border-t border-[var(--landing-fg)]/10.

p-4 · 16px
p-6 · 24px
p-8 · 32px
p-10 · 40px
p-12 · 48px
p-16 · 64px
p-20 · 80px
p-24 · 96px

Grid

Use a 12-column grid: grid grid-cols-12 gap-8. Editorial layouts favor 7/5 or 8/4 splits over symmetric 6/6.

05 — Motion

Motion

Hover lift

-translate-y-0.5 on primary CTAs.

Color fade

150ms ease for opacity & color.

Reveal

framer-motion: opacity 0→1, y 12→0, 0.6s ease.

All animation must respect @media (prefers-reduced-motion: reduce) — use useReducedMotion() from framer-motion.

06 — Components

Buttons

Primary

bg-[var(--landing-fg)] text-[var(--landing-bg)]
px-6 py-3.5 text-sm font-medium
hover:-translate-y-1 transition-transform

Secondary

border border-[var(--landing-fg)]
px-6 py-3.5 text-sm font-medium
hover:bg-[var(--landing-accent)]

Small / Uppercase

Ghost / Link

Learn more

07 — Components

Form inputs

Recipe

  • • Label: mono, 10px, uppercase, tracking 0.25em, opacity 0.6
  • • Input: border-[var(--landing-fg)]/20, transparent bg, 2.5/3 padding
  • • Focus: focus:border-[var(--landing-fg)] focus:outline-none
  • • Never use shadcn rounded inputs on landing/marketing surfaces

08 — Components

Surfaces & borders

Card · Default

bg-[var(--landing-bg)] · border /15

Card · Subtle

bg-[var(--landing-surface)] · for grouped content

Card · Inverted

For CTA blocks and emphasis only

Border opacity ladder: /10 for layout dividers, /15 for cards,/20 for inputs, /100 for emphasis.

09 — Components

Data display

SystemRiskStatusUpdated
Credit scoring v3HighApproved2 days ago
KYC anomalyMediumIn review5 hours ago
Fraud queue triageLowApproved1 week ago
ApprovedIn reviewBlocked

10 — Components

Feedback

Saved

Your changes were recorded.

Action required

A control failed enforcement.

Tip

Use sonner for transient toasts.

11 — Quality bar

Do's & don'ts

Do

  • ✓ Use design tokens (CSS variables) for every color.
  • ✓ Use Space Grotesk for all headings & CTAs.
  • ✓ Use mono uppercase 10–11px for eyebrows and IDs.
  • ✓ Use sharp corners on landing & marketing surfaces.
  • ✓ Match section rhythm: py-16 lg:py-20 + top border.
  • ✓ Keep hover effects subtle: lift or color fade.

Don't

  • ✗ Hard-code hex values (#2d3a2a).
  • ✗ Add purple gradients, drop shadows, or glassmorphism.
  • ✗ Use text-white / bg-black — break dark mode.
  • ✗ Use Inter, Poppins, or default sans for display.
  • ✗ Add bouncy spring animations or carousels.
  • ✗ Mix rounded shadcn cards with sharp landing surfaces.

12 — Motion library

Animated graphic elements

Copy-paste primitives for loading, progress, and ambient motion. Each element is dependency-free Tailwind + a tiny @keyframes rule you can drop into src/styles.css. All animations honor prefers-reduced-motion.

01 · loader

Pulsing dot

Status indicator for live / streaming / recording states.

tsx
<span className="relative flex h-3 w-3">
  <span className="absolute inline-flex h-full w-full rounded-full bg-emerald-500 opacity-60 animate-ping" />
  <span className="relative inline-flex h-3 w-3 rounded-full bg-emerald-500" />
</span>

02 · loader

Spinner ring

Default async spinner. Inherits currentColor.

tsx
<svg className="h-5 w-5 animate-spin" viewBox="0 0 24 24" fill="none">
  <circle cx="12" cy="12" r="10" stroke="currentColor" strokeOpacity="0.15" strokeWidth="2" />
  <path d="M22 12a10 10 0 0 0-10-10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>

03 · loader

Three-dot typing

AI / chat / 'someone is typing' affordance.

tsx
{/* tailwind config: add 'bounce-dot' keyframe (1.2s ease-in-out infinite) */}
<div className="flex items-end gap-1.5">
  {[0, 150, 300].map(d => (
    <span key={d} className="h-2 w-2 rounded-full bg-current animate-[bounce-dot_1.2s_ease-in-out_infinite]"
      style={{ animationDelay: `${d}ms` }} />
  ))}
</div>

04 · skeleton

Shimmer skeleton

Loading placeholder for cards, rows, and avatars.

tsx
<div className="relative h-3 w-full overflow-hidden bg-foreground/10">
  <div className="absolute inset-0 -translate-x-full bg-gradient-to-r from-transparent via-white/60 to-transparent
                  animate-[shimmer_1.8s_ease-in-out_infinite]" />
</div>
/* @keyframes shimmer { 100% { transform: translateX(100%); } } */

05 · progress

Indeterminate bar

Top-of-page progress for route changes and uploads.

tsx
<div className="relative h-1 w-full overflow-hidden bg-foreground/10">
  <div className="absolute inset-y-0 w-1/4 bg-foreground animate-[progress_1.6s_ease-in-out_infinite]" />
</div>
/* @keyframes progress { 0%{transform:translateX(-100%)} 100%{transform:translateX(400%)} } */

06 · success

Animated checkmark

Confirmation moment after a successful save / submit.

tsx
<svg viewBox="0 0 52 52" className="h-16 w-16 text-emerald-600">
  <circle cx="26" cy="26" r="24" fill="none" stroke="currentColor" strokeWidth="2"
    strokeDasharray="160" strokeDashoffset="160"
    style={{ animation: "draw 0.6s ease-out forwards" }} />
  <path d="M14 27 L23 36 L39 18" fill="none" stroke="currentColor" strokeWidth="2.5"
    strokeLinecap="round" strokeLinejoin="round"
    strokeDasharray="50" strokeDashoffset="50"
    style={{ animation: "draw 0.4s 0.5s ease-out forwards" }} />
</svg>
/* @keyframes draw { to { stroke-dashoffset: 0; } } */

07 · ambient

Floating gradient orb

Decorative hero element. Pair two with mix-blend-multiply.

tsx
<div className="relative h-full w-full overflow-hidden">
  <div className="absolute left-1/4 top-1/2 h-64 w-64 -translate-y-1/2 rounded-full
                  bg-emerald-300/60 blur-3xl animate-[blob_7s_ease-in-out_infinite]" />
  <div className="absolute right-1/4 top-1/2 h-64 w-64 -translate-y-1/2 rounded-full
                  bg-amber-200/60 blur-3xl animate-[blob_9s_1s_ease-in-out_infinite]" />
</div>
/* @keyframes blob {
  0%,100%{transform:translate(0,0) scale(1)}
  33%{transform:translate(30px,-20px) scale(1.1)}
  66%{transform:translate(-20px,15px) scale(0.95)} } */

08 · scanner

Scanning beam

Document analysis / AI inference visual.

tsx
<div className="relative overflow-hidden">
  <div className="absolute inset-x-0 h-px bg-emerald-500
                  shadow-[0_0_12px_2px_rgba(16,185,129,0.7)]
                  animate-[scan_2s_ease-in-out_infinite]" />
  {/* …content… */}
</div>
/* @keyframes scan { 0%,100%{transform:translateY(-100%)} 50%{transform:translateY(2400%)} } */

09 · ticker

Infinite marquee

GDPRHIPAASOC2ISO27001NISTCCPAGDPRHIPAASOC2ISO27001NISTCCPAGDPRHIPAASOC2ISO27001NISTCCPAGDPRHIPAASOC2ISO27001NISTCCPA

Logo strip, news ticker, status feed.

tsx
<div className="w-full overflow-hidden">
  <div className="flex w-max gap-8 whitespace-nowrap animate-[marquee_18s_linear_infinite]">
    {[...items, ...items].map((t, i) => <span key={i}>{t}</span>)}
  </div>
</div>
/* @keyframes marquee { to { transform: translateX(-50%); } } */

10 · typewriter

Blinking caret

magpie.audit

Pair with a typewriter hook for terminal-style input.

tsx
<span className="inline-block h-[1em] w-[2px] translate-y-0.5 bg-current
                animate-[caret_1s_steps(2)_infinite]" />
/* @keyframes caret { 50% { opacity: 0; } } */

11 · empty

Floating illustration

Gentle bob for empty-state and onboarding artwork.

tsx
<div className="animate-[float_4s_ease-in-out_infinite]">{/* svg */}</div>
/* @keyframes float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-6px)} } */

12 · orbit

Orbiting dot

Loading state for connection / sync flows.

tsx
<div className="relative h-20 w-20">
  <div className="absolute inset-0 rounded-full border border-foreground/20" />
  <div className="absolute left-1/2 top-1/2 h-2 w-2 -translate-x-1/2 -translate-y-1/2
                  rounded-full bg-current animate-[orbit_2.4s_linear_infinite]" />
</div>
/* @keyframes orbit {
  from { transform: rotate(0) translateX(40px) rotate(0); }
  to   { transform: rotate(360deg) translateX(40px) rotate(-360deg); } } */

13 — State gallery

Error, empty & system states

A catalogue of every dead-end the user can hit. Each card is a self-contained component — illustration, copy, and code — designed in the Magpie editorial style. Drop them into your route's errorComponent / notFoundComponent or render conditionally for empty data.

State · 404

Not found

404

Page not found

The URL doesn't match any route. Offer a path home and a search.

tsx
<div className="flex min-h-screen flex-col items-center justify-center gap-4 p-8">
  <h1 className="text-7xl font-medium tracking-tighter">404</h1>
  <p className="opacity-70">This page wandered off.</p>
  <Link to="/" className="border px-4 py-2">Take me home</Link>
</div>

State · 500

Server error

500

Something broke on our end

Unhandled exception. Always include a request ID for support.

tsx
<div className="mx-auto max-w-md p-8 text-center">
  <ServerCrash className="mx-auto h-10 w-10 text-destructive" />
  <h1 className="mt-4 text-2xl font-medium">Something broke</h1>
  <p className="mt-2 text-sm opacity-70">Request ID: {requestId}</p>
  <button onClick={() => router.invalidate()} className="mt-6 border px-4 py-2">
    Try again
  </button>
</div>

State · 503

Unavailable

503

We're upgrading the engine

Planned maintenance. Show ETA when known.

tsx
<div className="mx-auto max-w-md p-8 text-center">
  <Wrench className="mx-auto h-10 w-10 animate-[wiggle_1.4s_ease-in-out_infinite]" />
  <h1 className="mt-4 text-2xl font-medium">Back in a moment</h1>
  <p className="mt-2 text-sm opacity-70">
    Scheduled maintenance · ETA {eta}
  </p>
</div>

State · 403

Forbidden

403

You don't have access

Authenticated but not authorized. Suggest contacting an admin.

tsx
<div className="mx-auto max-w-md p-8 text-center">
  <ShieldAlert className="mx-auto h-10 w-10" />
  <h1 className="mt-4 text-2xl font-medium">Access denied</h1>
  <p className="mt-2 text-sm opacity-70">
    Ask your workspace admin for the {role} role.
  </p>
</div>

State · 401

Unauthorized

Sign in to continue

No active session. Preserve the original URL for post-login redirect.

tsx
<Navigate to="/auth" search={{ next: location.pathname }} replace />

State · EMPTY

No items

Inbox is clear

Use for empty lists, dashboards, and notification panels.

tsx
{items.length === 0 && (
  <div className="flex flex-col items-center gap-3 py-16 text-center">
    <Inbox className="h-10 w-10 opacity-60" />
    <p className="text-sm opacity-70">Nothing here yet.</p>
  </div>
)}

State · NO-RES

Search empty

0

No matches for that search

Surface filters that excluded results and offer to clear them.

tsx
<div className="py-12 text-center">
  <Search className="mx-auto h-8 w-8 opacity-60" />
  <p className="mt-3 text-sm opacity-70">
    No results for "{query}"
  </p>
  <button onClick={clearFilters} className="mt-3 text-xs underline">
    Clear filters
  </button>
</div>

State · OFFLINE

No connection

You're offline

Detect via navigator.onLine. Queue mutations and retry on reconnect.

tsx
useEffect(() => {
  const onOff = () => setOnline(navigator.onLine);
  window.addEventListener("online", onOff);
  window.addEventListener("offline", onOff);
  return () => {
    window.removeEventListener("online", onOff);
    window.removeEventListener("offline", onOff);
  };
}, []);

State · EXP

Session expired

00:00

You've been signed out

JWT expiry. Modal-style takeover with a sign-in CTA.

tsx
<Dialog open={expired}>
  <DialogContent>
    <Clock className="h-8 w-8" />
    <DialogTitle>Session expired</DialogTitle>
    <Button onClick={() => signIn()}>Sign in again</Button>
  </DialogContent>
</Dialog>

State · LOAD

Loading

Fetching data

Use for first-paint loading on dashboards and long lists.

tsx
{isLoading ? (
  <div className="space-y-2">
    {[90,70,80,55].map((w,i) => (
      <Skeleton key={i} style={{ width: `${w}%` }} className="h-3" />
    ))}
  </div>
) : <List data={data} />}

State · OK

Success

All done

Confirmation screen for multi-step flows and exports.

tsx
<div className="flex flex-col items-center gap-3 p-8 text-center">
  <CheckCircle2 className="h-12 w-12 text-emerald-600" />
  <h2 className="text-xl font-medium">All done</h2>
  <p className="text-sm opacity-70">Your report is ready.</p>
</div>

State · ERR

Inline error

Error 4801

Something went wrong

Form-level / component-level error with retry.

tsx
<Alert variant="destructive">
  <AlertTriangle className="h-4 w-4" />
  <AlertTitle>Couldn't save</AlertTitle>
  <AlertDescription>
    {error.message}
    <button onClick={retry} className="ml-2 underline">Retry</button>
  </AlertDescription>
</Alert>

State · LOCK

Locked

Upgrade to unlock

Paywall / feature flag gate. Plain copy, no scare tactics.

tsx
<div className="border-2 border-dashed p-8 text-center">
  <Lock className="mx-auto h-8 w-8 opacity-60" />
  <h3 className="mt-3 font-medium">Available on Team plan</h3>
  <Button onClick={openBilling} className="mt-4">Upgrade</Button>
</div>

State · SOON

Coming soon

In development

Use for placeholder routes and gated feature previews.

tsx
<div className="flex flex-col items-center gap-3 py-16">
  <div className="flex gap-1">
    {[0,150,300].map(d => (
      <span key={d} className="h-2 w-2 rounded-full bg-current animate-bounce"
        style={{ animationDelay: `${d}ms` }} />
    ))}
  </div>
  <p className="text-sm opacity-70">Coming soon</p>
</div>

State · UPL

No upload

Drop file

Start by adding a file

First-run state for uploads, imports, and integrations.

tsx
<label className="flex h-40 cursor-pointer flex-col items-center justify-center
                   border-2 border-dashed">
  <FileQuestion className="h-8 w-8 opacity-70" />
  <p className="mt-2 text-sm">Drop a file or click to browse</p>
  <input type="file" className="sr-only" onChange={handleFile} />
</label>

Magpie · Design System · Living document — update with every shipped pattern