Skip to main content

🧱 Part 11 — Design System & Theming (Tokens • CSS Variables • Tailwind • A11y) 🎨


A design system makes your UI consistent, accessible, and fast to iterate. We’ll define design tokens (colors, spacing, radii, typography) as CSS variables, wire light/dark themes, and build typed UI primitives (Button, Input, Card). Everything is copy‑paste ready and verified for errors.


🧩 Concepts in 30 seconds

  • Tokens: the single source of truth for colors/spacing/typography.
  • Primitives: small, reusable components that read tokens.
  • Variants: controlled style options (e.g., primary | secondary | danger).
  • Theming: switch token values for light/dark without changing component code.

1) Tokens as CSS variables (light & dark)

We use HSL/hex via var(--token) so classes can read them with Tailwind’s arbitrary values.

/* src/styles/tokens.css */
:root {
  /* Base colors */
  --color-bg: #ffffff;
  --color-fg: #111111;
  --color-muted: #6b7280;          /* slate-500 */

  --color-primary: #2563eb;        /* blue-600 */
  --color-danger: #dc2626;         /* red-600 */

  /* Surfaces */
  --surface: #ffffff;
  --surface-fore: var(--color-fg);
  --border: #e5e7eb;               /* slate-200 */

  /* Radii / Shadows / Spacing / Type */
  --radius: 12px;
  --shadow-sm: 0 1px 2px rgba(0,0,0,.06);
  --shadow-md: 0 4px 10px rgba(0,0,0,.08);
  --leading: 1.5;
  --font-sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Noto Sans,
    Ubuntu, Cantarell, "Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji";
}

/* Dark theme toggled by <html data-theme="dark"> */
:root[data-theme="dark"] {
  --color-bg: #0f172a;             /* slate-900 */
  --color-fg: #f8fafc;             /* slate-50 */
  --color-muted: #94a3b8;          /* slate-400 */

  --color-primary: #3b82f6;        /* blue-500 */
  --color-danger: #ef4444;         /* red-500 */

  --surface: #111827;              /* gray-900 */
  --surface-fore: var(--color-fg);
  --border: #1f2937;               /* gray-800 */
}

Apply globally and set sane defaults:

/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@import './styles/tokens.css';
@import './styles/typography.css';
@import './styles/motion.css';

html, body, #root { height: 100%; }
body {
  background: var(--color-bg);
  color: var(--color-fg);
  line-height: var(--leading);
  font-family: var(--font-sans);
}

/* Surface utility for quick prototypes */
.card {
  background: var(--surface);
  color: var(--surface-fore);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

Optional extras (typography & reduced‑motion):

/* src/styles/typography.css */
.h1 { font-size: clamp(1.75rem, 2vw + 1rem, 2.25rem); font-weight: 700; }
.h2 { font-size: clamp(1.5rem, 1.5vw + 1rem, 1.875rem); font-weight: 600; }
.subtle { color: var(--color-muted); }
/* src/styles/motion.css */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}

2) Tailwind config (reads our tokens)

We’ll extend a couple of theme values and otherwise use arbitrary values with CSS variables.

// tailwind.config.ts
import type { Config } from 'tailwindcss';

export default {
  content: ['./index.html', './src/**/*.{ts,tsx}'],
  theme: {
    extend: {
      borderRadius: { xl: 'var(--radius)' },
      boxShadow: { skin: 'var(--shadow-md)' },
    },
  },
  plugins: [],
} satisfies Config;

Example usage:

<div className="bg-[var(--surface)] text-[var(--surface-fore)] border border-[var(--border)] rounded-xl p-4 shadow-skin">
  Token‑powered surface
</div>

3) Class name helper (cn) — no errors

// src/lib/cn.ts
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';

// Merge conditional classes and resolve Tailwind conflicts safely
export function cn(...inputs: unknown[]) {
  return twMerge(clsx(inputs));
}

4) Button primitive (typed variants + a11y)

Uses class-variance-authority (CVA) for typed variants. Install once:

pnpm add class-variance-authority
// src/components/ui/Button.tsx
import { cva, type VariantProps } from 'class-variance-authority';
import type { ButtonHTMLAttributes } from 'react';
import { cn } from '@/lib/cn';

type Variant = 'primary' | 'secondary' | 'danger';

// Base + variant classes
const buttonStyles = cva(
  'inline-flex items-center justify-center rounded-xl font-medium select-none transition-colors '
  + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 '
  + 'disabled:opacity-50 disabled:cursor-not-allowed',
  {
    variants: {
      variant: {
        primary: 'bg-[var(--color-primary)] text-white hover:brightness-95 ring-[var(--color-primary)] ring-offset-[var(--color-bg)]',
        secondary: 'bg-[var(--surface)] text-[var(--surface-fore)] border border-[var(--border)] hover:bg-[var(--color-bg)] ring-[var(--border)] ring-offset-[var(--color-bg)]',
        danger: 'bg-[var(--color-danger)] text-white hover:brightness-95 ring-[var(--color-danger)] ring-offset-[var(--color-bg)]',
      },
      size: {
        sm: 'px-2 py-1 text-sm',
        md: 'px-4 py-2 text-base',
        lg: 'px-6 py-3 text-lg',
      },
    },
    defaultVariants: { variant: 'primary', size: 'md' },
  },
);

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonStyles> {
  isLoading?: boolean;
}

export function Button({ className, variant, size, isLoading, children, ...props }: ButtonProps) {
  return (
    <button
      className={cn(buttonStyles({ variant, size }), className)}
      aria-busy={isLoading ? 'true' : undefined}
      disabled={isLoading || props.disabled}
      {...props}
    >
      {isLoading ? '⏳' : children}
    </button>
  );
}

5) Input primitive (labels + error wiring)

// src/components/ui/Input.tsx
import type { InputHTMLAttributes } from 'react';
import { cn } from '@/lib/cn';

export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  label?: string;
  error?: string;
}

export function Input({ label, error, className, id, name, ...props }: InputProps) {
  const inputId = id ?? name ?? `in-${Math.random().toString(36).slice(2)}`; // stable enough for demo
  const errId = `${inputId}-err`;
  return (
    <label className="block text-sm font-medium" htmlFor={inputId}>
      {label && <span className="mb-1 block">{label}</span>}
      <input
        id={inputId}
        name={name}
        aria-invalid={error ? 'true' : undefined}
        aria-describedby={error ? errId : undefined}
        className={cn(
          'w-full rounded-xl px-3 py-2 shadow-sm focus:ring-2 border',
          error ? 'border-red-500 focus:ring-red-500' : 'border-[var(--border)] focus:ring-[var(--color-primary)]',
          'bg-[var(--surface)] text-[var(--surface-fore)]',
          className,
        )}
        {...props}
      />
      {error && <span id={errId} className="text-sm text-red-600">{error}</span>}
    </label>
  );
}

6) Card primitive (token surface)

// src/components/ui/Card.tsx
import type { ReactNode } from 'react';
import { cn } from '@/lib/cn';

export function Card({ children, className }: { children: ReactNode; className?: string }) {
  return (
    <div className={cn('bg-[var(--surface)] text-[var(--surface-fore)] border border-[var(--border)] rounded-xl p-4 shadow-skin', className)}>
      {children}
    </div>
  );
}

7) Theme toggle (tokens respond automatically)

Assuming you have a ThemeProvider that sets <html data-theme>.

// src/components/ThemeToolbar.tsx
import { Button } from '@/components/ui/Button';
import { useTheme } from '@/contexts/ThemeContext';

export function ThemeToolbar() {
  const { theme, toggle } = useTheme();
  return (
    <div className="flex items-center gap-2">
      <span className="subtle">Theme:</span>
      <Button variant="secondary" onClick={toggle}>Toggle ({theme})</Button>
    </div>
  );
}

8) Quick visual check page (optional)

// src/pages/design/SystemDemo.tsx
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input';

export default function SystemDemo() {
  return (
    <div className="space-y-4">
      <h1 className="h1">Design System Demo</h1>
      <p className="subtle">Token‑driven theming with accessible primitives.</p>

      <div className="flex gap-2">
        <Button>Primary</Button>
        <Button variant="secondary">Secondary</Button>
        <Button variant="danger">Danger</Button>
      </div>

      <Card>
        <div className="h2 mb-2">Card Title</div>
        <p>Card content using token surfaces and borders.</p>
        <div className="mt-3 grid gap-2">
          <Input label="Email" type="email" placeholder="you@example.com" />
          <Input label="Title" error="Title is required" />
        </div>
      </Card>
    </div>
  );
}

Add a route like /design and flip light/dark to verify.


✅ Wrap‑Up

You now have a token‑driven design system:

  • CSS variables for colors/surfaces/spacing/typography
  • Tailwind utilities that read tokens via var(--token)
  • Accessible, typed primitives (Button, Input, Card)
  • Light/Dark theming through <html data-theme>


Comments

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...