Back to Blog

Design System Engineering: Token-Based Theming, Component Library, Storybook, and Chromatic

Build a production design system: implement design token architecture with CSS custom properties, create a typed React component library, document with Storybook, automate visual regression testing with Chromatic, and publish with semantic versioning.

Viprasol Tech Team
October 13, 2026
13 min read

A design system is the single source of truth for your product's visual language. Without one, every team makes slightly different decisions about spacing, color, and typography, and the UI becomes inconsistent over time. With one, every product surface looks and feels cohesive โ€” and UI development is faster because components are built once and reused everywhere.

The engineering challenge: building a component library that's flexible enough to cover real use cases, type-safe enough that misuse is caught at compile time, and documented well enough that teams use it instead of building their own.


Design Token Architecture

Design tokens are named, versioned values that represent design decisions โ€” colors, spacing, typography, shadows. They're the foundation everything else builds on.

// packages/tokens/src/tokens.ts
// Source-of-truth token definitions

export const tokens = {
  color: {
    // Semantic tokens (what they mean, not what they look like)
    // Consumers use semantic tokens โ€” not raw color values
    primary: {
      50:  "#eff6ff",
      100: "#dbeafe",
      200: "#bfdbfe",
      300: "#93c5fd",
      400: "#60a5fa",
      500: "#3b82f6",
      600: "#2563eb",   // โ† Primary action color
      700: "#1d4ed8",
      800: "#1e40af",
      900: "#1e3a8a",
    },
    neutral: {
      0:   "#ffffff",
      50:  "#f8fafc",
      100: "#f1f5f9",
      200: "#e2e8f0",
      300: "#cbd5e1",
      400: "#94a3b8",
      500: "#64748b",
      600: "#475569",
      700: "#334155",
      800: "#1e293b",
      900: "#0f172a",
      1000: "#000000",
    },
    semantic: {
      // These map to primary/neutral above but carry meaning
      "text-primary":    "neutral.900",
      "text-secondary":  "neutral.500",
      "text-disabled":   "neutral.300",
      "text-inverse":    "neutral.0",
      "bg-base":         "neutral.0",
      "bg-subtle":       "neutral.50",
      "bg-muted":        "neutral.100",
      "border-default":  "neutral.200",
      "border-strong":   "neutral.400",
      "action-primary":  "primary.600",
      "action-hover":    "primary.700",
      "action-disabled": "primary.200",
    },
  },

  spacing: {
    0:   "0px",
    0.5: "2px",
    1:   "4px",
    1.5: "6px",
    2:   "8px",
    3:   "12px",
    4:   "16px",
    5:   "20px",
    6:   "24px",
    8:   "32px",
    10:  "40px",
    12:  "48px",
    16:  "64px",
    20:  "80px",
    24:  "96px",
  },

  typography: {
    fontFamily: {
      sans:  "'Inter', system-ui, -apple-system, sans-serif",
      mono:  "'JetBrains Mono', 'Cascadia Code', monospace",
    },
    fontSize: {
      xs:   ["12px", { lineHeight: "16px" }],
      sm:   ["14px", { lineHeight: "20px" }],
      base: ["16px", { lineHeight: "24px" }],
      lg:   ["18px", { lineHeight: "28px" }],
      xl:   ["20px", { lineHeight: "28px" }],
      "2xl":["24px", { lineHeight: "32px" }],
      "3xl":["30px", { lineHeight: "36px" }],
      "4xl":["36px", { lineHeight: "40px" }],
    },
    fontWeight: {
      normal:   400,
      medium:   500,
      semibold: 600,
      bold:     700,
    },
  },

  radius: {
    none: "0px",
    sm:   "4px",
    md:   "6px",
    lg:   "8px",
    xl:   "12px",
    "2xl":"16px",
    full: "9999px",
  },

  shadow: {
    sm:  "0 1px 2px 0 rgb(0 0 0 / 0.05)",
    md:  "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
    lg:  "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
    xl:  "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
  },
} as const;

// Generate CSS custom properties from tokens
export function generateCSSVariables(): string {
  const lines: string[] = [":root {"];

  // Colors
  for (const [name, value] of Object.entries(tokens.color.primary)) {
    lines.push(`  --color-primary-${name}: ${value};`);
  }
  for (const [name, value] of Object.entries(tokens.color.neutral)) {
    lines.push(`  --color-neutral-${name}: ${value};`);
  }

  // Spacing
  for (const [name, value] of Object.entries(tokens.spacing)) {
    lines.push(`  --spacing-${name.replace(".", "_")}: ${value};`);
  }

  // Border radius
  for (const [name, value] of Object.entries(tokens.radius)) {
    lines.push(`  --radius-${name}: ${value};`);
  }

  lines.push("}");
  return lines.join("\n");
}

Button Component

// packages/components/src/Button/Button.tsx
import React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../utils/cn";

// cva (class-variance-authority): type-safe variant management
const buttonVariants = cva(
  // Base styles โ€” applied to all buttons
  [
    "inline-flex items-center justify-center gap-2",
    "font-medium rounded-md transition-colors",
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-600 focus-visible:ring-offset-2",
    "disabled:pointer-events-none disabled:opacity-50",
  ],
  {
    variants: {
      variant: {
        primary:   "bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800",
        secondary: "bg-neutral-100 text-neutral-900 hover:bg-neutral-200 active:bg-neutral-300",
        outline:   "border border-neutral-300 bg-transparent text-neutral-900 hover:bg-neutral-50",
        ghost:     "bg-transparent text-neutral-700 hover:bg-neutral-100",
        destructive: "bg-red-600 text-white hover:bg-red-700",
      },
      size: {
        sm:  "h-8 px-3 text-sm",
        md:  "h-10 px-4 text-sm",
        lg:  "h-11 px-6 text-base",
        xl:  "h-12 px-8 text-base",
        icon:"h-10 w-10",
      },
    },
    defaultVariants: {
      variant: "primary",
      size: "md",
    },
  }
);

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  loading?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  asChild?: boolean;
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  (
    {
      className,
      variant,
      size,
      loading = false,
      leftIcon,
      rightIcon,
      disabled,
      children,
      ...props
    },
    ref
  ) => {
    return (
      <button
        ref={ref}
        className={cn(buttonVariants({ variant, size }), className)}
        disabled={disabled || loading}
        aria-busy={loading}
        {...props}
      >
        {loading ? (
          <svg
            className="h-4 w-4 animate-spin"
            viewBox="0 0 24 24"
            fill="none"
            aria-hidden="true"
          >
            <circle
              className="opacity-25"
              cx="12" cy="12" r="10"
              stroke="currentColor" strokeWidth="4"
            />
            <path
              className="opacity-75"
              fill="currentColor"
              d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
            />
          </svg>
        ) : leftIcon}
        {children}
        {!loading && rightIcon}
      </button>
    );
  }
);

Button.displayName = "Button";

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Storybook Stories

// packages/components/src/Button/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./Button";
import { PlusIcon, ArrowRightIcon } from "lucide-react";

const meta: Meta<typeof Button> = {
  title: "Components/Button",
  component: Button,
  tags: ["autodocs"],  // Auto-generate docs from component props
  argTypes: {
    variant: {
      control: "select",
      description: "Visual style of the button",
    },
    size: {
      control: "select",
    },
    loading: {
      control: "boolean",
    },
    disabled: {
      control: "boolean",
    },
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

// Canonical stories โ€” these are what Chromatic will visually diff
export const Primary: Story = {
  args: {
    variant: "primary",
    children: "Create project",
  },
};

export const AllVariants: Story = {
  render: () => (
    <div className="flex flex-wrap gap-3">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Destructive</Button>
    </div>
  ),
};

export const AllSizes: Story = {
  render: () => (
    <div className="flex items-center gap-3">
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
      <Button size="xl">Extra Large</Button>
    </div>
  ),
};

export const WithIcons: Story = {
  render: () => (
    <div className="flex gap-3">
      <Button leftIcon={<PlusIcon className="h-4 w-4" />}>New project</Button>
      <Button rightIcon={<ArrowRightIcon className="h-4 w-4" />}>Continue</Button>
    </div>
  ),
};

export const Loading: Story = {
  args: { loading: true, children: "Saving..." },
};

export const Disabled: Story = {
  args: { disabled: true, children: "Unavailable" },
};

Chromatic Visual Regression Testing

# .github/workflows/chromatic.yml
name: Chromatic Visual Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  chromatic:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Required for Chromatic to compare baselines

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"

      - run: npm ci

      - name: Publish to Chromatic
        uses: chromaui/action@v1
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          buildScriptName: "build-storybook"
          # Auto-accept changes on main (they become the new baseline)
          autoAcceptChanges: "main"
          # On PRs: only exit-zero if changes are approved in Chromatic UI
          exitZeroOnChanges: false
          # Only run for changed stories (faster CI)
          onlyChanged: true

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Package Publishing

// packages/components/package.json
{
  "name": "@viprasol/components",
  "version": "2.1.0",
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "sideEffects": false,
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts --clean",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
  },
  "peerDependencies": {
    "react": ">=18",
    "react-dom": ">=18"
  }
}

See Also


Working With Viprasol

A design system is a long-term investment in UI consistency and development velocity. Our engineers build token-based design systems, typed React component libraries, Storybook documentation, and Chromatic visual regression pipelines that catch UI regressions before they reach production โ€” reducing the "it looks different on staging" problem to near zero.

Frontend engineering โ†’ | Start a project โ†’

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.