All skills
wshobson avatar

/design-system-patterns

@be57c0b
by Seth Hobsonwshobson/agents40k stars
4,281

Build scalable design systems with design tokens, theming infrastructure, and component architecture patterns. Use when creating design tokens, implementing theme switching, building component libraries, or establishing design system foundations.

Use this Skill: https://skilld.dev/gh/wshobson/agents/design-system-patterns

This session only. Nothing lands on disk.

referencestheming-architecture.md

≈3.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Theming Architecture

Overview

A robust theming system enables applications to support multiple visual appearances (light/dark modes, brand themes) while maintaining consistency and developer experience.

CSS Custom Properties Architecture

Base Setup

/* 1. Define the token contract */
:root {
  /* Color scheme */
  color-scheme: light dark;

  /* Base tokens that don't change */
  --font-sans: Inter, system-ui, sans-serif;
  --font-mono: "JetBrains Mono", monospace;

  /* Animation tokens */
  --duration-fast: 150ms;
  --duration-normal: 250ms;
  --duration-slow: 400ms;
  --ease-default: cubic-bezier(0.4, 0, 0.2, 1);

  /* Z-index scale */
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-modal: 300;
  --z-popover: 400;
  --z-tooltip: 500;
}

/* 2. Light theme (default) */
:root,
[data-theme="light"] {
  --color-bg: #ffffff;
  --color-bg-subtle: #f8fafc;
  --color-bg-muted: #f1f5f9;
  --color-bg-emphasis: #0f172a;

  --color-text: #0f172a;
  --color-text-muted: #475569;
  --color-text-subtle: #94a3b8;

  --color-border: #e2e8f0;
  --color-border-muted: #f1f5f9;

  --color-accent: #3b82f6;
  --color-accent-hover: #2563eb;
  --color-accent-muted: #dbeafe;

  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}

/* 3. Dark theme */
[data-theme="dark"] {
  --color-bg: #0f172a;
  --color-bg-subtle: #1e293b;
  --color-bg-muted: #334155;
  --color-bg-emphasis: #f8fafc;

  --color-text: #f8fafc;
  --color-text-muted: #94a3b8;
  --color-text-subtle: #64748b;

  --color-border: #334155;
  --color-border-muted: #1e293b;

  --color-accent: #60a5fa;
  --color-accent-hover: #93c5fd;
  --color-accent-muted: #1e3a5f;

  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.5);
}

/* 4. System preference detection */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    /* Inherit dark theme values */
    --color-bg: #0f172a;
    /* ... other dark values */
  }
}

Using Tokens in Components

.card {
  background: var(--color-bg-subtle);
  border: 1px solid var(--color-border);
  border-radius: 0.5rem;
  box-shadow: var(--shadow-sm);
  padding: 1.5rem;
}

.card-title {
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: 1.125rem;
  font-weight: 600;
}

.card-description {
  color: var(--color-text-muted);
  margin-top: 0.5rem;
}

.button-primary {
  background: var(--color-accent);
  color: white;
  transition: background var(--duration-fast) var(--ease-default);
}

.button-primary:hover {
  background: var(--color-accent-hover);
}

React Theme Provider

Complete Implementation

// theme-provider.tsx
import * as React from "react";

type Theme = "light" | "dark" | "system";
type ResolvedTheme = "light" | "dark";

interface ThemeProviderProps {
  children: React.ReactNode;
  defaultTheme?: Theme;
  storageKey?: string;
  attribute?: "class" | "data-theme";
  enableSystem?: boolean;
  disableTransitionOnChange?: boolean;
}

interface ThemeProviderState {
  theme: Theme;
  resolvedTheme: ResolvedTheme;
  setTheme: (theme: Theme) => void;
  toggleTheme: () => void;
}

const ThemeProviderContext = React.createContext<
  ThemeProviderState | undefined
>(undefined);

export function ThemeProvider({
  children,
  defaultTheme = "system",
  storageKey = "theme",
  attribute = "data-theme",
  enableSystem = true,
  disableTransitionOnChange = false,
}: ThemeProviderProps) {
  const [theme, setThemeState] = React.useState<Theme>(() => {
    if (typeof window === "undefined") return defaultTheme;
    return (localStorage.getItem(storageKey) as Theme) || defaultTheme;
  });

  const [resolvedTheme, setResolvedTheme] =
    React.useState<ResolvedTheme>("light");

  // Get system preference
  const getSystemTheme = React.useCallback((): ResolvedTheme => {
    if (typeof window === "undefined") return "light";
    return window.matchMedia("(prefers-color-scheme: dark)").matches
      ? "dark"
      : "light";
  }, []);

  // Apply theme to DOM
  const applyTheme = React.useCallback(
    (newTheme: ResolvedTheme) => {
      const root = document.documentElement;

      // Disable transitions temporarily
      if (disableTransitionOnChange) {
        const css = document.createElement("style");
        css.appendChild(
          document.createTextNode(
            `*,*::before,*::after{transition:none!important}`,
          ),
        );
        document.head.appendChild(css);

        // Force repaint
        (() => window.getComputedStyle(document.body))();

        // Remove after a tick
        setTimeout(() => {
          document.head.removeChild(css);
        }, 1);
      }

      // Apply attribute
      if (attribute === "class") {
        root.classList.remove("light", "dark");
        root.classList.add(newTheme);
      } else {
        root.setAttribute(attribute, newTheme);
      }

      // Update color-scheme for native elements
      root.style.colorScheme = newTheme;

      setResolvedTheme(newTheme);
    },
    [attribute, disableTransitionOnChange],
  );

  // Handle theme changes
  React.useEffect(() => {
    const resolved = theme === "system" ? getSystemTheme() : theme;
    applyTheme(resolved);
  }, [theme, applyTheme, getSystemTheme]);

  // Listen for system theme changes
  React.useEffect(() => {
    if (!enableSystem || theme !== "system") return;

    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

    const handleChange = () => {
      applyTheme(getSystemTheme());
    };

    mediaQuery.addEventListener("change", handleChange);
    return () => mediaQuery.removeEventListener("change", handleChange);
  }, [theme, enableSystem, applyTheme, getSystemTheme]);

  // Persist to localStorage
  const setTheme = React.useCallback(
    (newTheme: Theme) => {
      localStorage.setItem(storageKey, newTheme);
      setThemeState(newTheme);
    },
    [storageKey],
  );

  const toggleTheme = React.useCallback(() => {
    setTheme(resolvedTheme === "light" ? "dark" : "light");
  }, [resolvedTheme, setTheme]);

  const value = React.useMemo(
    () => ({
      theme,
      resolvedTheme,
      setTheme,
      toggleTheme,
    }),
    [theme, resolvedTheme, setTheme, toggleTheme],
  );

  return (
    <ThemeProviderContext.Provider value={value}>
      {children}
    </ThemeProviderContext.Provider>
  );
}

export function useTheme() {
  const context = React.useContext(ThemeProviderContext);
  if (context === undefined) {
    throw new Error("useTheme must be used within a ThemeProvider");
  }
  return context;
}

Theme Toggle Component

// theme-toggle.tsx
import { Moon, Sun, Monitor } from "lucide-react";
import { useTheme } from "./theme-provider";

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    <div className="flex items-center gap-1 rounded-lg bg-muted p-1">
      <button
        onClick={() => setTheme("light")}
        className={`rounded-md p-2 ${
          theme === "light" ? "bg-background shadow-sm" : ""
        }`}
        aria-label="Light theme"
      >
        <Sun className="h-4 w-4" />
      </button>
      <button
        onClick={() => setTheme("dark")}
        className={`rounded-md p-2 ${
          theme === "dark" ? "bg-background shadow-sm" : ""
        }`}
        aria-label="Dark theme"
      >
        <Moon className="h-4 w-4" />
      </button>
      <button
        onClick={() => setTheme("system")}
        className={`rounded-md p-2 ${
          theme === "system" ? "bg-background shadow-sm" : ""
        }`}
        aria-label="System theme"
      >
        <Monitor className="h-4 w-4" />
      </button>
    </div>
  );
}

Multi-Brand Theming

Brand Token Structure

/* Brand A - Corporate Blue */
[data-brand="corporate"] {
  --brand-primary: #0066cc;
  --brand-primary-hover: #0052a3;
  --brand-secondary: #f0f7ff;
  --brand-accent: #00a3e0;

  --brand-font-heading: "Helvetica Neue", sans-serif;
  --brand-font-body: "Open Sans", sans-serif;

  --brand-radius: 0.25rem;
  --brand-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Brand B - Modern Startup */
[data-brand="startup"] {
  --brand-primary: #7c3aed;
  --brand-primary-hover: #6d28d9;
  --brand-secondary: #faf5ff;
  --brand-accent: #f472b6;

  --brand-font-heading: "Poppins", sans-serif;
  --brand-font-body: "Inter", sans-serif;

  --brand-radius: 1rem;
  --brand-shadow: 0 4px 12px rgba(124, 58, 237, 0.15);
}

/* Brand C - Minimal */
[data-brand="minimal"] {
  --brand-primary: #171717;
  --brand-primary-hover: #404040;
  --brand-secondary: #fafafa;
  --brand-accent: #171717;

  --brand-font-heading: "Space Grotesk", sans-serif;
  --brand-font-body: "IBM Plex Sans", sans-serif;

  --brand-radius: 0;
  --brand-shadow: none;
}

Accessibility Considerations

Reduced Motion

@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-fast: 0ms;
    --duration-normal: 0ms;
    --duration-slow: 0ms;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

High Contrast Mode

@media (prefers-contrast: high) {
  :root {
    --color-text: #000000;
    --color-text-muted: #000000;
    --color-bg: #ffffff;
    --color-border: #000000;
    --color-accent: #0000ee;
  }

  [data-theme="dark"] {
    --color-text: #ffffff;
    --color-text-muted: #ffffff;
    --color-bg: #000000;
    --color-border: #ffffff;
    --color-accent: #ffff00;
  }
}

Forced Colors

@media (forced-colors: active) {
  .button {
    border: 2px solid currentColor;
  }

  .card {
    border: 1px solid CanvasText;
  }

  .link {
    text-decoration: underline;
  }
}

Server-Side Rendering

Preventing Flash of Unstyled Content

// Inline script to prevent FOUC
const themeScript = `
  (function() {
    const theme = localStorage.getItem('theme') || 'system';
    const isDark = theme === 'dark' ||
      (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);

    document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
    document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
  })();
`;

// In Next.js layout - note: inline scripts should be properly sanitized in production
export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <script
          // Only use for trusted, static content
          // For dynamic content, use a sanitization library
          dangerouslySetInnerHTML={{ __html: themeScript }}
        />
      </head>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  );
}

Testing Themes

// theme.test.tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ThemeProvider, useTheme } from "./theme-provider";

function TestComponent() {
  const { theme, setTheme, resolvedTheme } = useTheme();
  return (
    <div>
      <span data-testid="theme">{theme}</span>
      <span data-testid="resolved">{resolvedTheme}</span>
      <button onClick={() => setTheme("dark")}>Set Dark</button>
    </div>
  );
}

describe("ThemeProvider", () => {
  it("should default to system theme", () => {
    render(
      <ThemeProvider>
        <TestComponent />
      </ThemeProvider>,
    );

    expect(screen.getByTestId("theme")).toHaveTextContent("system");
  });

  it("should switch to dark theme", async () => {
    const user = userEvent.setup();

    render(
      <ThemeProvider>
        <TestComponent />
      </ThemeProvider>,
    );

    await user.click(screen.getByText("Set Dark"));
    expect(screen.getByTestId("theme")).toHaveTextContent("dark");
    expect(document.documentElement).toHaveAttribute("data-theme", "dark");
  });
});

Resources

Source: SKILL.md on GitHub

No alerts17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The analyzed design system patterns skill focuses exclusively on frontend architectures, design tokens, and components with no hazardous behaviors identified.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer6mo

    4 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at be57c0b. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 days ago.

Activeupdated 4 months ago
  • TypeScript
  • React
  • design-tokens
  • theming
  • css-custom-properties
  • component-library
  • design-system
  • figma
  • style-dictionary

README badge

README badge for wshobson/agents/design-system-patterns

Provides patterns and best practices for building design systems with design tokens, theming infrastructure, and component architecture. Covers design token hierarchies (primitive, semantic, component), CSS custom properties for theme switching, Figma-to-code synchronization, and component library patterns for multi-brand and light/dark mode systems.

Generated from the current SKILL.md.

Does this skill cover Figma-to-code token synchronization?
Yes. The skill includes patterns for Figma tokens integration, Style Dictionary configuration, and CI/CD pipelines to automate token updates from design tools to code.
What frameworks does this skill assume?
The skill uses React as the primary example (theme context providers) but the design token and CSS custom properties patterns are framework-agnostic and apply to any web platform.
Does this cover dark mode and system preference detection?
Yes. The skill includes patterns for dynamic theme switching, system preference detection using prefers-color-scheme, reduced motion, and high contrast modes.
What is the token hierarchy this skill recommends?
Three tiers: primitive tokens (raw values like colors and sizes), semantic tokens (contextual meaning like text-primary), and component tokens (specific usage like button-bg).
Does this skill address multi-platform token generation?
Yes. The skill covers token generation for multiple platforms including CSS, iOS, and Android through the token pipeline section.

Generated from the current SKILL.md. These answers refresh after source changes.