Skip to main content

Overview

Stride Design System uses Tailwind CSS v4 with semantic tokens and class-variance-authority (cva) for managing component variants. This approach provides type-safe styling with excellent developer experience.

Tailwind CSS v4 Integration

Configuration

Stride uses Tailwind CSS v4 with inline theme configuration and CSS custom properties:
src/styles.css
@import "tailwindcss";
@import "./app/brands.css";

/* Explicitly declare sources for better scanning */
@source "./**/*.{js,jsx,ts,tsx,mdx}";

/* Configure dark mode */
@theme {
  --color-scheme: light dark;
}

Semantic Tokens

All components use semantic CSS custom properties for theming:
/* Background */
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--bg-brand-muted: #e3f2fd;

/* Text */
--text-primary: #1a1a1a;
--text-secondary: #666666;
--text-tertiary: #999999;

/* Interactive */
--interactive-primary: #2563eb;
--interactive-primary-hover: #1d4ed8;
--interactive-primary-active: #1e40af;

The cn() Utility Function

Stride provides a utility function for merging class names with Tailwind CSS:
src/lib/utils.ts
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

How it Works

  1. clsx: Conditionally constructs className strings
  2. twMerge: Intelligently merges Tailwind classes, removing conflicts

Usage Examples

import { cn } from '@/lib/utils';

// Merge multiple classes
const className = cn('text-blue-500', 'font-bold', 'hover:text-blue-600');

// Conditional classes
const buttonClass = cn(
  'px-4 py-2 rounded',
  isActive && 'bg-blue-500 text-white',
  isDisabled && 'opacity-50 cursor-not-allowed'
);

Class Variance Authority (cva)

Overview

cva provides type-safe variant management for component styling. Every Stride component uses cva for consistent variant handling.

Basic Pattern

import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

const componentVariants = cva(
  // Base classes (always applied)
  ['base-class-1', 'base-class-2'],
  {
    variants: {
      variant: {
        default: 'variant-classes',
        secondary: 'other-classes',
      },
      size: {
        sm: 'small-classes',
        md: 'medium-classes',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'md',
    },
  }
);

Real-World Examples

Button Component

Here’s how the Button component uses cva with semantic tokens:
src/components/ui/Button/Button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
  [
    "inline-flex items-center justify-center gap-2",
    "whitespace-nowrap font-medium font-sans cursor-pointer",
    "focus-visible:outline-none focus-visible:ring-2",
    "disabled:opacity-50 disabled:cursor-not-allowed",
    "transition-all",
    // Semantic tokens
    "[transition-duration:var(--transition-normal)]",
    "[border-radius:var(--radius-button)]",
  ],
  {
    variants: {
      variant: {
        primary: [
          "[background-color:var(--interactive-primary)]",
          "[color:var(--interactive-primary-text)]",
          "hover:[background-color:var(--interactive-primary-hover)]",
          "active:[background-color:var(--interactive-primary-active)]",
        ],
        secondary: [
          "[background-color:var(--interactive-secondary)]",
          "[color:var(--text-primary)]",
          "[border:1px_solid_var(--border-primary)]",
        ],
        ghost: [
          "[background-color:var(--interactive-ghost)]",
          "[color:var(--text-secondary)]",
          "hover:[color:var(--text-primary)]",
        ],
      },
      size: {
        sm: [
          "[height:var(--button-height-sm)]",
          "[padding:var(--button-padding-sm)]",
          "[font-size:var(--font-size-xs)]",
        ],
        md: [
          "[height:var(--button-height-md)]",
          "[padding:var(--button-padding-md)]",
          "[font-size:var(--font-size-sm)]",
        ],
        lg: [
          "[height:var(--button-height-lg)]",
          "[padding:var(--button-padding-lg)]",
          "[font-size:var(--font-size-md)]",
        ],
      },
    },
    defaultVariants: {
      variant: "primary",
      size: "md",
    },
  }
);

export interface ButtonProps
  extends AriaButtonProps,
    VariantProps<typeof buttonVariants> {
  className?: string;
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, ...props }, ref) => {
    return (
      <AriaButton
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    );
  }
);

Badge Component

The Badge component demonstrates multiple variant types:
src/components/ui/Badge/Badge.tsx
const badgeVariants = cva(
  [
    "inline-flex items-center justify-center font-sans",
    "border rounded-full whitespace-nowrap",
    "[transition-duration:var(--transition-normal)]",
  ],
  {
    variants: {
      variant: {
        default: [
          "[background-color:var(--bg-secondary)]",
          "[color:var(--text-primary)]",
        ],
        success: [
          "[background-color:var(--status-success-bg)]",
          "[color:var(--status-success-text)]",
          "[border-color:var(--status-success)]",
        ],
        warning: [
          "[background-color:var(--status-warning-bg)]",
          "[color:var(--status-warning-text)]",
        ],
        danger: [
          "[background-color:var(--status-danger-bg)]",
          "[color:var(--status-danger-text)]",
        ],
      },
      size: {
        sm: ["[height:var(--badge-height-sm)]", "px-2"],
        md: ["[height:var(--badge-height-md)]", "px-3"],
        lg: ["[height:var(--badge-height-lg)]", "px-4"],
      },
    },
    defaultVariants: {
      variant: "default",
      size: "md",
    },
  }
);

Compound Variants

Use compound variants for combinations of variant states:
src/components/ui/Card/Card.tsx
const cardVariants = cva("base-classes", {
  variants: {
    size: { sm: "...", md: "...", lg: "..." },
    hasFooter: { true: "", false: "" },
  },
  compoundVariants: [
    {
      size: "sm",
      hasFooter: true,
      class: "[padding-bottom:var(--spacing-md)]",
    },
    {
      size: "md",
      hasFooter: true,
      class: "[padding-bottom:var(--spacing-md)]",
    },
  ],
});

Custom Component Styling

Extending Components

All Stride components accept a className prop for customization:
import { Button } from 'stride-ds';

<Button 
  variant="primary"
  className="shadow-lg hover:shadow-xl"
>
  Custom Button
</Button>

Creating Custom Variants

You can create your own variants using the same pattern:
import { cva } from 'class-variance-authority';
import { cn } from 'stride-ds';

const myComponentVariants = cva(
  'base-styles',
  {
    variants: {
      theme: {
        ocean: 'bg-blue-500 text-white',
        forest: 'bg-green-500 text-white',
        sunset: 'bg-orange-500 text-white',
      },
    },
  }
);

function MyComponent({ theme }) {
  return (
    <div className={cn(myComponentVariants({ theme }))}>
      Content
    </div>
  );
}

Best Practices

Use Semantic Tokens

Always use CSS custom properties instead of hardcoded values for better theming support.

Leverage cn() Utility

Use the cn() function to merge classes intelligently and avoid conflicts.

Type-Safe Variants

Extend VariantProps to get full TypeScript support for your component props.

Consistent Patterns

Follow the established cva pattern across all custom components for consistency.

Dark Mode Support

Stride automatically supports dark mode through CSS custom properties:
:root {
  --bg-primary: #ffffff;
  --text-primary: #1a1a1a;
}

:root.dark {
  --bg-primary: #1a1a1a;
  --text-primary: #ffffff;
}
No component code changes needed - tokens automatically adapt!

Next Steps

TypeScript Guide

Learn about TypeScript patterns and type exports

Contributing

Create your own components following our patterns

Build docs developers (and LLMs) love