Skip to main content

Composables

Composables

Reusable utility functions and helpers

Tags: utility helpers functions

The theme library provides utility functions and helpers for common tasks.

Class Utilities​

cn()​

Merge class names with Tailwind conflict resolution:

<script>
  import { cn } from '@classic-homes/theme-svelte';
</script>

<div
  class={cn(
    'px-4 py-2 rounded-md',
    variant === 'primary' && 'bg-primary text-primary-foreground',
    variant === 'secondary' && 'bg-secondary text-secondary-foreground',
    className
  )}
>
  Content
</div>

The cn() function combines clsx for conditional classes with tailwind-merge for intelligent Tailwind class conflict resolution.

Examples​

import { cn } from '@classic-homes/theme-svelte';

// Conditional classes
cn('base-class', isActive && 'active-class');
// → 'base-class active-class' or 'base-class'

// Override conflicting Tailwind classes
cn('px-4 py-2', 'px-6');
// → 'py-2 px-6' (px-4 is removed)

// Object syntax
cn('base', { active: isActive, disabled: isDisabled });

// Array syntax
cn(['class1', 'class2'], condition && 'class3');

// Combining all
cn(
  'base-styles',
  variant === 'primary' && 'bg-primary',
  { 'opacity-50': disabled },
  className // user override
);

Type Utilities​

Component Types​

Export your own types for reusable patterns:

// types.ts
import type { DataTableColumn, Tab, NavItem, NavSection } from '@classic-homes/theme-svelte';

export type { DataTableColumn, Tab, NavItem, NavSection };

Common Exported Types​

// Navigation
interface NavItem {
  id: string;
  name: string;
  href?: string;
  icon?: string;
  active?: boolean;
  children?: NavItem[];
  roles?: string[];
  external?: boolean;
}

interface NavSection {
  id: string;
  title?: string;
  items: NavItem[];
  collapsible?: boolean;
  expanded?: boolean;
}

// Data Table
interface DataTableColumn<T> {
  id: string;
  header: string;
  accessor: keyof T | ((row: T) => unknown);
  sortable?: boolean;
  width?: string;
  align?: 'left' | 'center' | 'right';
  format?: (value: unknown, row: T) => string;
}

// Tabs
interface Tab {
  id: string;
  label: string;
  disabled?: boolean;
}

// Toast
interface Toast {
  id: string;
  message: string;
  type: 'success' | 'error' | 'warning' | 'info';
  title?: string;
  duration?: number;
}

// User
interface User {
  name: string;
  email?: string;
  initials?: string;
  avatar?: string;
  roles?: string[];
}

Creating Custom Composables​

Media Query Hook​

// composables/useMediaQuery.svelte.ts
export function useMediaQuery(query: string) {
  let matches = $state(false);

  $effect(() => {
    if (typeof window === 'undefined') return;

    const mediaQuery = window.matchMedia(query);
    matches = mediaQuery.matches;

    const handler = (e: MediaQueryListEvent) => {
      matches = e.matches;
    };

    mediaQuery.addEventListener('change', handler);
    return () => mediaQuery.removeEventListener('change', handler);
  });

  return {
    get matches() {
      return matches;
    },
  };
}

Usage:

<script>
  import { useMediaQuery } from './composables/useMediaQuery.svelte';

  const isMobile = useMediaQuery('(max-width: 768px)');
</script>

{#if isMobile.matches}
  <MobileNav />
{:else}
  <DesktopNav />
{/if}

Click Outside​

// composables/useClickOutside.svelte.ts
export function useClickOutside(getElement: () => HTMLElement | null, callback: () => void) {
  $effect(() => {
    const element = getElement();
    if (!element) return;

    const handler = (event: MouseEvent) => {
      if (!element.contains(event.target as Node)) {
        callback();
      }
    };

    document.addEventListener('click', handler);
    return () => document.removeEventListener('click', handler);
  });
}

Usage:

<script>
  import { useClickOutside } from './composables/useClickOutside.svelte';

  let menuRef = $state<HTMLElement | null>(null);
  let open = $state(false);

  useClickOutside(
    () => menuRef,
    () => (open = false)
  );
</script>

<div bind:this={menuRef}>
  {#if open}
    <div class="menu">Menu content</div>
  {/if}
</div>

Debounced Value​

// composables/useDebounce.svelte.ts
export function useDebounce<T>(getValue: () => T, delay: number) {
  let debouncedValue = $state<T>(getValue());

  $effect(() => {
    const value = getValue();
    const timeout = setTimeout(() => {
      debouncedValue = value;
    }, delay);

    return () => clearTimeout(timeout);
  });

  return {
    get value() {
      return debouncedValue;
    },
  };
}

Usage:

<script>
  import { useDebounce } from './composables/useDebounce.svelte';

  let searchInput = $state('');
  const debouncedSearch = useDebounce(() => searchInput, 300);

  $effect(() => {
    if (debouncedSearch.value) {
      search(debouncedSearch.value);
    }
  });
</script>

<input bind:value={searchInput} placeholder="Search..." />

Best Practices​

  1. Use cn() for all className merging - Prevents Tailwind conflicts
  2. Import types for reuse - Consistent interfaces across app
  3. Create composables for repeated logic - DRY principle
  4. Use $effect for side effects - Proper cleanup with return functions
On this page