Styling System

A comprehensive guide to styling components with TailwindCSS, variants, presets, and the cn() utility.

Overview

The styling system combines utility-first CSS with powerful variant definitions and global theming.

All components accept a class prop that supports strings, arrays with conditionals, and objects. The styling system is built on TailwindCSS utility classes, enhanced with the cn() utility for intelligent class merging, and integrated with the preset system for global theming.

Class Prop Formats

String

class="bg-primary text-white"

Array

class={['base', isActive && 'active']}

Object

class={{ 'active': isActive }}

Mixed

class={['base', condition && 'class', klass]}

TailwindCSS Utilities

Use Tailwind utility classes for 90% of your styling needs with full responsive support.

Common Patterns

<!-- Layout & spacing -->
<Card.Root class="max-w-sm p-4">
  <Card.Header>
    <Card.Title class="text-lg font-semibold">Title</Card.Title>
  </Card.Header>
</Card.Root>

<!-- Interactive states -->
<button class="bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90 active:bg-primary/100">
  Action
</button>

<!-- Responsive -->
<h2 class="text-2xl md:text-3xl lg:text-4xl">Responsive heading</h2>

<!-- With opacity -->
<div class="bg-foreground/10">Subtle background</div>

The cn() Utility

Intelligent class merging with automatic conflict resolution using clsx and tailwind-merge.

Usage Examples

The cn() utility combines clsx for conditional class handling with tailwind-merge for intelligent Tailwind class conflict resolution. When conflicting utility classes are provided, the last one wins.

import { cn } from '@svelte-atoms/core/utils';

// Resolves conflicts automatically
cn('px-2 py-1', 'px-4'); 
// Result: 'py-1 px-4'

// Handles conditionals
cn('base', isActive && 'active', false && 'ignored');
// Result: 'base active'

// Merges arrays
cn(['text-sm', 'font-medium'], 'text-lg');
// Result: 'font-medium text-lg'

CSS Custom Properties

Use theme color tokens for consistent, theme-aware styling across your application. The library uses a token system following shadcn/ui conventions for maximum compatibility and familiarity.

Available Color Tokens

background / foreground
primary / primary-foreground
secondary / secondary-foreground
muted / muted-foreground
accent / accent-foreground
destructive / destructive-foreground
card / card-foreground
border / input / ring

Color Token Usage

<!-- Color tokens -->
<div class="bg-background text-foreground">Background color</div>
<Button class="bg-primary text-primary-foreground">Primary button</Button>

<!-- With opacity -->
<div class="bg-foreground/10 text-foreground/50">Subtle styling</div>

<!-- Borders and shadows -->
<Card.Root class="border-border border shadow-lg">Card</Card.Root>

<!-- All available colors -->
<div class="bg-secondary text-secondary-foreground">Secondary</div>
<div class="bg-muted text-muted-foreground">Muted</div>
<div class="bg-accent text-accent-foreground">Accent</div>
<div class="bg-destructive text-destructive-foreground">Destructive</div>

Conditional Styling

Apply classes conditionally based on component state or props using arrays and ternary operators.

Usage Examples

<script>
  let isOpen = $state(false);
  let isActive = $state(true);
</script>

<!-- Array with conditions -->
<Collapsible.Body 
  class={[
    'pointer-events-none h-0 opacity-0', 
    isOpen && 'pointer-events-auto h-auto opacity-100'
  ]}
>
  Content
</Collapsible.Body>

<!-- Ternary -->
<Tab.Root class={isActive ? 'opacity-100' : 'opacity-50'}>
  Tab content
</Tab.Root>

<!-- Multiple conditions -->
<Button 
  class={[
    'base-button',
    isActive && 'bg-primary',
    isDisabled && 'opacity-50 pointer-events-none',
    size === 'large' && 'px-6 py-3'
  ]}
>
  Button
</Button>

Variant System

Define reusable component variants with full TypeScript support and bond state access.

The defineVariants() utility provides a powerful way to create component styling variations at the component level. Variants can define multiple styling dimensions (size, variant, state), support compound variants for conditional styling when multiple conditions match, and access component bond state for reactive styling.

This example shows how to define variants at the component level. For global variants that apply across your entire application, use the preset system instead.

Basic Variant Definition

<script lang="ts">
  import { defineVariants } from '@svelte-atoms/core/utils';
  import { HtmlAtom } from '@svelte-atoms/core';

  const buttonVariants = defineVariants({
    class: 'inline-flex items-center justify-center rounded-md font-medium',
    variants: {
      variant: {
        primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/90',
        ghost: 'hover:bg-accent hover:text-accent-foreground'
      },
      size: {
        sm: 'h-8 px-3 text-sm',
        md: 'h-10 px-4',
        lg: 'h-12 px-6 text-lg'
      }
    },
    defaults: {
      variant: 'primary',
      size: 'md'
    }
  });

  let { variant, size, ...props } = $props();
</script>

<HtmlAtom variants={buttonVariants} {variant} {size} {...props}>
  {@render children?.()}
</HtmlAtom>

Key Features

Base Classes

Define base styling that applies to all variants

Multiple Dimensions

Combine variant, size, state, and custom dimensions

Default Values

Set sensible defaults for all variant dimensions

Type Safety

Automatic TypeScript inference for all variant options

Compound Variants

Apply styling when multiple conditions match

Attribute Support

Return aria-*, data-*, and other HTML attributes

Compound Variants

Apply additional styling when multiple variant conditions match simultaneously.

Usage Example

Compound variants allow you to define styling that only applies when specific combinations of variant values are active. This is useful for edge cases or special styling requirements that go beyond single-dimension variants. Compounds can also set HTML attributes like role and aria-* attributes.

const alertVariants = defineVariants({
  class: 'rounded-lg p-4 border',
  variants: {
    variant: {
      error: 'bg-destructive/10 border-destructive/50 text-destructive'
    },
    size: {
      lg: 'text-lg p-6'
    }
  },
  compounds: [
    {
      variant: 'error',
      size: 'lg',
      class: 'font-bold',
      role: 'alert',
      'aria-live': 'assertive'
    }
  ]
});

Reactive Variants

Access component bond state for dynamic, reactive variant styling.

Usage Example

When you need variants to respond to component state, pass a function to defineVariants() that receives the component's bond. This enables reactive styling based on internal state like isOpen, isActive, or isDisabled.

const accordionVariants = defineVariants((bond) => ({
  class: 'border rounded-md transition-all',
  variants: {
    state: {
      open: {
        class: bond?.state?.isOpen ? 'bg-primary/5 border-primary' : 'bg-card',
        'aria-expanded': bond?.state?.isOpen,
        'data-state': bond?.state?.isOpen ? 'open' : 'closed'
      }
    }
  }
}));

The $preset Placeholder

Control exactly where preset classes are inserted in your class arrays.

Usage Example

When using presets, the special '$preset' string in your class array will be replaced with preset classes at that exact position. If no '$preset' placeholder is present, preset classes are automatically prepended to your class array.

<!-- In your component -->
<HtmlAtom
  preset="button"
  class={[
    'component-defaults',
    '$preset',  // Replaced with preset classes
    klass       // User classes override
  ]}
/>

<!-- Without $preset (preset at start) -->
<HtmlAtom
  preset="button"
  class={['component-classes', klass]}
/>

<!-- Result: preset-classes component-classes user-classes -->

Inline Styles

Use inline styles sparingly, only for truly dynamic values that cannot be expressed with classes.

Inline styles should be reserved for values that change frequently or cannot be predetermined, such as dynamic widths, transforms, or opacity values driven by user interaction or animation. For static styling, always prefer Tailwind utility classes.

When to Use Inline Styles:

Dynamic dimensions (width, height) based on state
Transform values for animations (translate, rotate, scale)
Opacity transitions not covered by Tailwind
Static colors, padding, margins (use Tailwind)
Fixed dimensions or spacing (use Tailwind)

Best Practices

Guidelines for effective styling that leads to maintainable, performant applications.

1.

Prefer Tailwind Utilities

Use TailwindCSS classes for 90% of styling needs. They're optimized, purged in production, and provide consistent design tokens.

2.

User Classes Override Last

Always place user-provided classes (like the class prop) last in your class array to ensure they can override component defaults and preset styles.

3.

Use Presets for Consistency

Define component defaults once in presets rather than repeating classes across your application. This makes theme changes easier.

4.

Leverage cn() for Merging

Always use cn() when merging classes. It handles conflicts intelligently and works with conditionals.

5.

Avoid Inline Styles

Reserve inline styles for truly dynamic values only. Static styling should always use utility classes for better performance and maintainability.

6.

Keep Specificity Low

Avoid !important and overly specific selectors. Let the cascade and class order handle overrides naturally.

7.

Use Semantic Color Tokens

Prefer semantic tokens like bg-primary over specific colors like bg-blue-500. This ensures theme compatibility.

8.

Extract Reusable Variants

When you find yourself repeating the same variant combinations, extract them into a defineVariants() definition.