Skip to main content

Component Prop Patterns

Stride Design System components follow consistent TypeScript patterns for type safety and developer experience.

VariantProps Pattern

All Stride components use Class Variance Authority (CVA) for variant-based styling.

Basic Pattern

import { cva, type VariantProps } from 'class-variance-authority';

const componentVariants = cva('base-styles', {
  variants: {
    variant: {
      primary: 'primary-styles',
      secondary: 'secondary-styles'
    },
    size: {
      sm: 'small-styles',
      md: 'medium-styles',
      lg: 'large-styles'
    }
  },
  defaultVariants: {
    variant: 'primary',
    size: 'md'
  }
});

// Extract variant types
export interface ComponentProps 
  extends VariantProps<typeof componentVariants> {
  // Additional props
}

Real Example: Button

import { cva, type VariantProps } from 'class-variance-authority';
import { type ButtonProps as AriaButtonProps } from 'react-aria-components';

const buttonVariants = cva(
  'inline-flex items-center justify-center gap-2 font-medium',
  {
    variants: {
      variant: {
        primary: 'bg-primary text-white',
        secondary: 'bg-secondary text-gray-900',
        ghost: 'bg-transparent text-gray-700',
        destructive: 'bg-red-600 text-white'
      },
      size: {
        sm: 'h-8 px-3 text-xs',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-md'
      }
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md'
    }
  }
);

export interface ButtonProps
  extends AriaButtonProps,
    VariantProps<typeof buttonVariants> {
  children?: React.ReactNode;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  className?: string;
}

React Aria Integration

Stride components extend React Aria Components for accessibility.

Extending React Aria Props

import {
  Button as AriaButton,
  type ButtonProps as AriaButtonProps
} from 'react-aria-components';
import { type VariantProps } from 'class-variance-authority';

// Component extends React Aria props
export interface ButtonProps
  extends AriaButtonProps,           // React Aria props
    VariantProps<typeof buttonVariants> { // CVA variants
  // Custom props
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

Common React Aria Props

React Aria provides these props automatically:
interface AriaButtonProps {
  // State
  isDisabled?: boolean;
  
  // Events
  onPress?: (e: PressEvent) => void;
  onPressStart?: (e: PressEvent) => void;
  onPressEnd?: (e: PressEvent) => void;
  onPressChange?: (isPressed: boolean) => void;
  onPressUp?: (e: PressEvent) => void;
  
  // Behavior
  preventFocusOnPress?: boolean;
  autoFocus?: boolean;
  
  // Standard HTML
  id?: string;
  'aria-label'?: string;
  'aria-labelledby'?: string;
  // ... and more
}

forwardRef Pattern

All Stride components support ref forwarding.

Standard Pattern

import React from 'react';
import { Button as AriaButton } from 'react-aria-components';

export interface ButtonProps {
  // props
}

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

Button.displayName = 'Button';

Usage with Ref

import { Button } from 'stride-ds';
import { useRef } from 'react';

function MyComponent() {
  const buttonRef = useRef<HTMLButtonElement>(null);

  const handleClick = () => {
    buttonRef.current?.focus();
  };

  return (
    <Button ref={buttonRef} onClick={handleClick}>
      Click Me
    </Button>
  );
}

HTMLAttributes Extension

Components that don’t use React Aria extend standard HTML attributes.

Pattern

import { type HTMLAttributes } from 'react';

export interface CardProps 
  extends HTMLAttributes<HTMLDivElement> {
  variant?: 'elevated' | 'outlined';
}

export const Card: React.FC<CardProps> = ({
  className,
  variant = 'elevated',
  ...props
}) => {
  return (
    <div
      className={cn(cardVariants({ variant }), className)}
      {...props}
    />
  );
};

Omit Pattern for Customization

Use Omit to exclude conflicting props from base components.

Example: Input with TextField

import {
  TextField as AriaTextField,
  type TextFieldProps as AriaTextFieldProps
} from 'react-aria-components';
import { type VariantProps } from 'class-variance-authority';

// Omit 'children' since we handle it differently
export interface InputProps
  extends Omit<AriaTextFieldProps, 'children'>,
    VariantProps<typeof inputVariants> {
  label?: string;
  placeholder?: string;
  helperText?: string;
  errorMessage?: string;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

Complex Component Example

Here’s a complete example showing all patterns:
import React from 'react';
import {
  Input as AriaInput,
  Label as AriaLabel,
  Text as AriaText,
  TextField as AriaTextField,
  type TextFieldProps as AriaTextFieldProps
} from 'react-aria-components';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from 'stride-ds';

// Define variants with CVA
const inputVariants = cva(
  'w-full px-3 border rounded-full',
  {
    variants: {
      size: {
        sm: 'h-8 text-xs px-3',
        md: 'h-10 text-sm px-3',
        lg: 'h-12 text-md px-4'
      },
      variant: {
        default: 'border-gray-300',
        error: 'border-red-500',
        success: 'border-green-500'
      }
    },
    defaultVariants: {
      size: 'md',
      variant: 'default'
    }
  }
);

// Define component props
export interface InputProps
  extends Omit<AriaTextFieldProps, 'children'>,
    VariantProps<typeof inputVariants> {
  label?: string;
  placeholder?: string;
  helperText?: string;
  errorMessage?: string;
  className?: string;
  inputClassName?: string;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
}

// Component with forwardRef
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
  (
    {
      className,
      inputClassName,
      size,
      variant,
      label,
      placeholder,
      helperText,
      errorMessage,
      leftIcon,
      rightIcon,
      isRequired,
      isDisabled,
      ...props
    },
    ref
  ) => {
    const computedVariant = errorMessage ? 'error' : variant;
    const displayHelperText = errorMessage || helperText;

    return (
      <AriaTextField
        className={cn('w-full', className)}
        isRequired={isRequired}
        isDisabled={isDisabled}
        {...props}
      >
        {label && (
          <AriaLabel>
            {label}
            {isRequired && <span className="text-red-500 ml-1">*</span>}
          </AriaLabel>
        )}

        <div className="relative">
          {leftIcon && (
            <div className="absolute left-3 top-1/2 -translate-y-1/2">
              {leftIcon}
            </div>
          )}

          <AriaInput
            ref={ref}
            className={cn(
              inputVariants({ size, variant: computedVariant }),
              leftIcon && 'pl-10',
              rightIcon && 'pr-10',
              inputClassName
            )}
            placeholder={placeholder}
          />

          {rightIcon && (
            <div className="absolute right-3 top-1/2 -translate-y-1/2">
              {rightIcon}
            </div>
          )}
        </div>

        {displayHelperText && (
          <AriaText slot={errorMessage ? 'errorMessage' : 'description'}>
            {displayHelperText}
          </AriaText>
        )}
      </AriaTextField>
    );
  }
);

Input.displayName = 'Input';

Type Exports

Always export your component types:
// Export the component
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(...);

// Export the props interface
export type { ButtonProps };

// Export variant type if useful
export type ButtonVariant = VariantProps<typeof buttonVariants>['variant'];
export type ButtonSize = VariantProps<typeof buttonVariants>['size'];

Usage in Applications

Basic Usage

import { Button, type ButtonProps } from 'stride-ds';

function MyComponent() {
  return (
    <Button variant="primary" size="md">
      Click Me
    </Button>
  );
}

Extending Component Types

import { Button, type ButtonProps } from 'stride-ds';

// Extend for custom wrapper
interface MyButtonProps extends ButtonProps {
  loading?: boolean;
  loadingText?: string;
}

function MyButton({ loading, loadingText, children, ...props }: MyButtonProps) {
  return (
    <Button {...props} isDisabled={loading || props.isDisabled}>
      {loading ? loadingText : children}
    </Button>
  );
}

Type-Safe Variant Usage

import { Button, type ButtonVariant } from 'stride-ds';

function VariantSelector() {
  const variants: ButtonVariant[] = ['primary', 'secondary', 'ghost', 'destructive'];
  
  return (
    <>
      {variants.map((variant) => (
        <Button key={variant} variant={variant}>
          {variant}
        </Button>
      ))}
    </>
  );
}

Best Practices

  1. Always use VariantProps: Ensures type safety with CVA variants
  2. Extend React Aria Components: Inherit accessibility props automatically
  3. Use forwardRef: Enable ref forwarding for DOM access
  4. Export all types: Make types available for consumers
  5. Set displayName: Improves debugging and dev tools experience
  6. Use Omit for conflicts: Remove conflicting props when extending
  7. Document custom props: Add JSDoc comments for custom props

Build docs developers (and LLMs) love