Default Components Styling

Learn how to style AudioUI's default components using the built-in theming system, CSS variables, and theme management utilities.

AudioUI's default components (Knob, Slider, Button, CycleButton, Keys) use a CSS variable-based theming system optimized for realtime audio applications. This system provides a clean, idiomatic way to customize component colors and styles without React Context or JavaScript overhead. For a high-level overview of theming on vector components, see Theming Features in the Vector introduction.

Note: This styling system applies to AudioUI's built-in default components. Fully customized components built with Control Primitives and custom view components use their own independent styling and theming systems.

Why a CSS Variable System?

Audio applications have strict performance requirements. The CSS variable approach ensures:

  • Zero React overhead: Theme changes don't trigger re-renders
  • Automatic updates: CSS handles theme changes instantly
  • Dark mode support: Automatic adaptation via CSS .dark class
  • Animation support: Colors can be animated smoothly
  • Framework agnostic: Works with any CSS-capable framework

How It Works

The color system uses a three-tier resolution hierarchy:

  1. Component prop (color prop on individual components)
  2. Global CSS variable (--audioui-primary-color)
  3. Adaptive default (--audioui-adaptive-default-color - white in dark mode, black in light mode)

When you set a color, AudioUI automatically computes variants (primary50, primary20) using CSS color-mix(). These variants are used for layered effects, borders, and visual depth.

Basic Usage

Setting Global Theme

Use the theme utility functions to set a global theme that applies to all components:

GlobalTheme.tsx
import { useEffect } from "react";
import { setThemeColor, setThemeRoundness } from "@cutoff/audio-ui-react";
import { Knob, Button } from "@cutoff/audio-ui-react";
 
export default function GlobalThemeExample() {
  useEffect(() => {
    // Set global theme (affects all components)
    setThemeColor("#3b82f6"); // Blue
    setThemeRoundness(0.3); // Slightly rounded
  }, []);
 
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Knob value={0.5} onChange={(e) => {}} label="Cutoff" />
      <Button value={false} onChange={(e) => {}} label="Power" />
    </div>
  );
}

Using Predefined Colors

AudioUI provides predefined theme colors for convenience:

PredefinedColors.tsx
import { useEffect } from "react";
import { setThemeColor, themeColors } from "@cutoff/audio-ui-react";
import { Knob } from "@cutoff/audio-ui-react";
 
export default function PredefinedColorsExample() {
  useEffect(() => {
    // Use predefined colors
    setThemeColor(themeColors.blue);
    // Or: setThemeColor(themeColors.purple);
    // Or: setThemeColor(themeColors.orange);
  }, []);
 
  return <Knob value={0.5} onChange={(e) => {}} label="Volume" />;
}

Available predefined colors: default, blue, orange, pink, green, purple, yellow.

Per-Component Colors

Override the global theme for individual components:

PerComponentColors.tsx
import { Knob, Slider } from "@cutoff/audio-ui-react";
 
export default function PerComponentColorsExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Cutoff"
        color="#3b82f6" // Blue
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Resonance"
        color="#ef4444" // Red
      />
      <Slider
        value={0.7}
        onChange={(e) => {}}
        label="Volume"
        color="hsl(160, 95%, 44%)" // Green
      />
    </div>
  );
}

Color Formats

The color prop accepts any valid CSS color value:

ColorFormats.tsx
import { Knob } from "@cutoff/audio-ui-react";
 
export default function ColorFormatsExample() {
  return (
    <div style={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
      <Knob value={0.5} onChange={(e) => {}} label="Named" color="blue" />
      <Knob value={0.5} onChange={(e) => {}} label="Hex" color="#FF5500" />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="RGB"
        color="rgb(255, 85, 0)"
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="HSL"
        color="hsl(20, 100%, 50%)"
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="CSS Variable"
        color="var(--my-custom-color)"
      />
    </div>
  );
}

Color Variants

AudioUI automatically generates color variants from your primary color:

  • primary50: 50% opacity variant (for backgrounds, tracks)
  • primary20: 20% opacity variant (for subtle effects)

These variants are computed using CSS color-mix(), so they update automatically when the primary color changes. Components use these variants for layered visual effects:

ColorVariants.tsx
import { Knob, Slider } from "@cutoff/audio-ui-react";
 
export default function ColorVariantsExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      {/* Knob uses primary50 for background arc, primary for foreground */}
      <Knob value={0.5} onChange={(e) => {}} label="Knob" color="#3b82f6" />
      {/* Slider uses primary50 for track, primary for fill */}
      <Slider
        value={0.7}
        onChange={(e) => {}}
        label="Slider"
        color="#3b82f6"
        orientation="vertical"
      />
    </div>
  );
}

Adaptive Default Colors

When no color is specified, components use an adaptive default that automatically switches between light and dark modes:

  • Light mode: Near-black (hsl(0, 0%, 10%))
  • Dark mode: Near-white (hsl(0, 0%, 96%))

This ensures components are always visible regardless of your application's theme:

AdaptiveDefault.tsx
import { Knob } from "@cutoff/audio-ui-react";
 
export default function AdaptiveDefaultExample() {
  // No color prop - uses adaptive default
  return <Knob value={0.5} onChange={(e) => {}} label="Adaptive" />;
}

CSS-Only Customization

You can customize themes using pure CSS without JavaScript:

CSSOnlyTheme.tsx
export default function CSSOnlyThemeExample() {
  return (
    <>
      <style>{`
        .my-theme {
          --audioui-primary-color: hsl(280, 80%, 60%);
          --audioui-roundness-base: 0.5;
        }
      `}</style>
      <div className="my-theme">
        <Knob value={0.5} onChange={(e) => {}} label="Custom Theme" />
      </div>
    </>
  );
}

Or using inline styles:

InlineCSSTheme.tsx
import { Knob } from "@cutoff/audio-ui-react";
 
export default function InlineCSSThemeExample() {
  return (
    <div
      style={{
        "--audioui-primary-color": "hsl(200, 100%, 50%)",
        "--audioui-roundness-base": "0.4",
      } as React.CSSProperties}
    >
      <Knob value={0.5} onChange={(e) => {}} label="Inline Theme" />
    </div>
  );
}

Roundness

Control the roundness of component corners using the roundness prop or global theme:

Roundness.tsx
import { Knob, Button } from "@cutoff/audio-ui-react";
 
export default function RoundnessExample() {
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Square"
        roundness={0.0}
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Rounded"
        roundness={0.5}
      />
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Fully Rounded"
        roundness={1.0}
      />
    </div>
  );
}

Set global roundness:

GlobalRoundness.tsx
import { useEffect } from "react";
import { setThemeRoundness } from "@cutoff/audio-ui-react";
 
export default function GlobalRoundnessExample() {
  useEffect(() => {
    setThemeRoundness(0.3); // Applies to all components
  }, []);
 
  return <Knob value={0.5} onChange={(e) => {}} label="Themed" />;
}

Dynamic Theme Switching

Themes can be changed dynamically at runtime:

DynamicTheme.tsx
import { useState } from "react";
import { setThemeColor, themeColors } from "@cutoff/audio-ui-react";
import { Knob } from "@cutoff/audio-ui-react";
 
export default function DynamicThemeExample() {
  const [currentTheme, setCurrentTheme] = useState(themeColors.blue);
 
  const themes = [
    { name: "Blue", color: themeColors.blue },
    { name: "Purple", color: themeColors.purple },
    { name: "Orange", color: themeColors.orange },
    { name: "Green", color: themeColors.green },
  ];
 
  const handleThemeChange = (color: string) => {
    setThemeColor(color);
    setCurrentTheme(color);
  };
 
  return (
    <div>
      <div style={{ display: "flex", gap: "10px", marginBottom: "20px" }}>
        {themes.map((theme) => (
          <button
            key={theme.name}
            onClick={() => handleThemeChange(theme.color)}
            style={{
              padding: "8px 16px",
              backgroundColor:
                currentTheme === theme.color ? theme.color : "#e5e7eb",
              color: currentTheme === theme.color ? "white" : "black",
              border: "none",
              borderRadius: "4px",
              cursor: "pointer",
            }}
          >
            {theme.name}
          </button>
        ))}
      </div>
      <Knob value={0.5} onChange={(e) => {}} label="Themed Knob" />
    </div>
  );
}

Color Resolution in Practice

Understanding the resolution hierarchy helps when debugging theme issues:

  1. Component prop takes precedence: If you pass color="red" to a Knob, it uses red regardless of global theme
  2. Global theme is fallback: If no color prop, components use --audioui-primary-color
  3. Adaptive default is final fallback: If no global theme is set, components use the adaptive default
ResolutionExample.tsx
import { useEffect } from "react";
import { setThemeColor, themeColors } from "@cutoff/audio-ui-react";
import { Knob } from "@cutoff/audio-ui-react";
 
export default function ResolutionExample() {
  useEffect(() => {
    setThemeColor(themeColors.blue); // Global theme
  }, []);
 
  return (
    <div style={{ display: "flex", gap: "20px" }}>
      {/* Uses global blue theme */}
      <Knob value={0.5} onChange={(e) => {}} label="Global Theme" />
      {/* Overrides with red */}
      <Knob
        value={0.5}
        onChange={(e) => {}}
        label="Override"
        color="red"
      />
    </div>
  );
}

Scope: Default Components Only

This styling system applies exclusively to AudioUI's default components:

  • Knob - All variants (abstract, simplest, plainCap, iconCap)
  • Slider - All variants (abstract, trackless, trackfull, stripless)
  • Button - All variants
  • CycleButton - All variants
  • Keys - All key styles

Custom components built with Control Primitives and custom view components do not use this styling system. They implement their own styling and theming independently.

Best Practices

  1. Set global theme once: Use setThemeColor() in your app's root component or layout
  2. Use predefined colors: themeColors provides consistent, tested color values
  3. Override sparingly: Use per-component colors only when needed for specific UI elements
  4. CSS variables for scoped themes: Use CSS variables when you need theme scoping (e.g., different themes for different sections)
  5. Test in both modes: Always verify your colors work in both light and dark modes

Next Steps