Skeleton v3 + Tailwind CSS 4 + Svelte 5. The modern stack for rapid, beautiful UI development.


🚀 Quick Setup

1. Install Dependencies

npm install -D @skeletonlabs/skeleton @skeletonlabs/skeleton-svelte tailwindcss @tailwindcss/vite

2. Configure Tailwind 4

/* app.css */
@import "tailwindcss";
@import "@skeletonlabs/skeleton";
@import "@skeletonlabs/skeleton-svelte";
@import "@skeletonlabs/skeleton/themes/cerberus";
 
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';

3. Vite Config

// vite.config.ts
import { sveltekit } from "@sveltejs/kit/vite"
import { defineConfig } from "vite"
import tailwindcss from "@tailwindcss/vite"
 
export default defineConfig({
  plugins: [tailwindcss(), sveltekit()],
})

4. HTML Theme

<!-- app.html -->
<html lang="en" data-theme="cerberus"></html>

🎨 Theme System

Built-in Themes

ThemeVibeBest For
cerberusProfessional, cleanSaaS, dashboards
catppuccinSoft, pastelCreative apps
vintageWarm, retroNiche products
wintryCool, crispWinter themes
modernNeutral, flexibleGeneral use

Custom Theme

/* app.css */
[data-theme="custom"] {
  --color-primary-50: #f0f9ff;
  --color-primary-500: #0ea5e9;
  --color-primary-950: #082f49;
 
  --color-surface-50: #ffffff;
  --color-surface-100: #f8fafc;
  --color-surface-900: #0f172a;
 
  --radius-base: 0.5rem;
  --radius-container: 1rem;
}

🧩 Component Patterns

Buttons

<script>
  import { Button } from '@skeletonlabs/skeleton-svelte'
</script>
 
<!-- Presets -->
<Button preset="filled">Filled</Button>
<Button preset="tonal">Tonal</Button>
<Button preset="outlined">Outlined</Button>
<Button preset="ghost">Ghost</Button>
 
<!-- With Icons -->
<Button preset="filled">
  <PlusIcon class="size-4 mr-2" />
  Add Item
</Button>
 
<!-- Sizes -->
<Button preset="filled" classes="btn-sm">Small</Button>
<Button preset="filled">Default</Button>
<Button preset="filled" classes="btn-lg">Large</Button>
 
<!-- States -->
<Button preset="filled" disabled>Disabled</Button>
<Button preset="filled" loading>Loading</Button>

Direct classes (no component import):

<button class="btn preset-filled">Button</button>
<button class="btn preset-tonal-primary">Primary Tonal</button>
<button class="btn preset-outlined-error">Error Outlined</button>

Cards

<!-- Basic Card -->
<div class="card preset-filled-surface-100-900 p-4">
  <h3 class="h3">Card Title</h3>
  <p class="opacity-60">Card content here</p>
</div>
 
<!-- Outlined Card -->
<div class="card preset-outlined-surface-200-800 p-4">
  Content
</div>
 
<!-- Interactive Card -->
<div class="card preset-filled-surface-100-900
            hover:preset-tonal
            cursor-pointer
            transition-all">
  Hover me
</div>
 
<!-- Header + Content -->
<div class="card">
  <header class="card-header flex justify-between items-center">
    <h4 class="h4">Settings</h4>
    <button class="btn-icon preset-tonal">
      <XIcon class="size-4" />
    </button>
  </header>
  <section class="p-4">
    Content
  </section>
  <footer class="card-footer flex justify-end gap-2">
    <button class="btn preset-tonal">Cancel</button>
    <button class="btn preset-filled-primary">Save</button>
  </footer>
</div>

Forms

<script>
  import { Segment, Switch } from '@skeletonlabs/skeleton-svelte'
</script>
 
<!-- Input with label -->
<label class="label">
  <span class="label-text">Email</span>
  <input
    type="email"
    class="input"
    placeholder="you@example.com"
  />
</label>
 
<!-- Floating label -->
<label class="floating-label">
  <input type="text" class="input" placeholder=" " />
  <span>Username</span>
</label>
 
<!-- Select -->
<select class="select">
  <option value="">Choose...</option>
  <option value="1">Option 1</option>
</select>
 
<!-- Switch -->
<Switch name="notifications" checked={enabled}>
  {#snippet children()}Enable notifications{/snippet}
</Switch>
 
<!-- Segment (Radio group) -->
<Segment name="plan" value={selectedPlan}>
  {#snippet children()}
    <Segment.Item value="free">Free</Segment.Item>
    <Segment.Item value="pro">Pro</Segment.Item>
    <Segment.Item value="enterprise">Enterprise</Segment.Item>
  {/snippet}
</Segment>
 
<!-- Textarea -->
<textarea class="textarea" rows="4" placeholder="Description..."></textarea>

Modals & Overlays

<script>
  import { Modal } from '@skeletonlabs/skeleton-svelte'
 
  let open = $state(false)
</script>
 
<button class="btn preset-filled" onclick={() => open = true}>
  Open Modal
</button>
 
<Modal
  bind:open
  triggerBase="btn preset-tonal"
  contentBase="card preset-filled-surface-100-900 p-4 w-full max-w-md"
  backdropClasses="backdrop-blur-sm"
>
  {#snippet trigger()}Open{/snippet}
  {#snippet content()}
    <header class="flex justify-between items-center mb-4">
      <h3 class="h3">Confirm Action</h3>
      <button class="btn-icon preset-tonal" onclick={() => open = false}>
        <XIcon class="size-4" />
      </button>
    </header>
    <p class="mb-4">Are you sure you want to proceed?</p>
    <footer class="flex justify-end gap-2">
      <button class="btn preset-tonal" onclick={() => open = false}>
        Cancel
      </button>
      <button class="btn preset-filled-error" onclick={confirm}>
        Delete
      </button>
    </footer>
  {/snippet}
</Modal>

<script>
  import { AppBar, AppShell, Navigation } from '@skeletonlabs/skeleton-svelte'
</script>
 
<!-- App Shell -->
<AppShell>
  {#snippet header()}
    <AppBar>
      {#snippet lead()}
        <h1 class="h4 font-bold">My App</h1>
      {/snippet}
      {#snippet trail()}
        <button class="btn-icon preset-tonal">
          <UserIcon class="size-5" />
        </button>
      {/snippet}
    </AppBar>
  {/snippet}
 
  {#snippet sidebarLeft()}
    <Navigation.Rail>
      <Navigation.Tile label="Home" href="/">
        <HomeIcon class="size-6" />
      </Navigation.Tile>
      <Navigation.Tile label="Settings" href="/settings">
        <SettingsIcon class="size-6" />
      </Navigation.Tile>
    </Navigation.Rail>
  {/snippet}
 
  {#snippet page()}
    <main class="p-4">
      <slot />
    </main>
  {/snippet}
</AppShell>

Data Display

<script>
  import { Table, Paginator } from '@skeletonlabs/skeleton-svelte'
</script>
 
<!-- Table -->
<Table
  data={users}
  columns={[
    { field: 'name', header: 'Name' },
    { field: 'email', header: 'Email' },
    {
      field: 'status',
      header: 'Status',
      render: (row) => row.status === 'active'
        ? '<span class="badge preset-filled-success">Active</span>'
        : '<span class="badge preset-filled-error">Inactive</span>'
    }
  ]}
/>
 
<!-- Badges -->
<span class="badge preset-filled-primary">New</span>
<span class="badge preset-tonal-secondary">Draft</span>
<span class="badge preset-outlined-warning">Pending</span>
 
<!-- Avatar -->
<Avatar
  src="/avatar.jpg"
  name="John Doe"
  classes="size-10"
/>
 
<Avatar
  initials="JD"
  classes="preset-filled-primary"
/>
 
<!-- Progress -->
<Progress
  value={60}
  max={100}
  meter="bg-primary-500"
  track="bg-surface-200-800"
/>

🎨 Utility Classes

Color Shortcuts

<!-- Background -->
<div class="preset-filled-primary">Primary fill</div>
<div class="preset-tonal-secondary">Secondary tonal</div>
<div class="preset-outlined-tertiary">Tertiary outlined</div>
 
<!-- Text -->
<p class="text-primary-500">Primary text</p>
<p class="text-surface-600-400">Adaptive text</p>
 
<!-- Border -->
<div class="border-primary-500">Primary border</div>
<div class="border-surface-200-800">Adaptive border</div>

Spacing Tokens

1 = 0.25rem (4px)
2 = 0.5rem  (8px)
3 = 0.75rem (12px)
4 = 1rem    (16px)
5 = 1.25rem (20px)
6 = 1.5rem  (24px)
8 = 2rem    (32px)
10 = 2.5rem (40px)
12 = 3rem   (48px)

Typography Scale

<h1 class="h1">Heading 1</h1>
<h2 class="h2">Heading 2</h2>
<h3 class="h3">Heading 3</h3>
<h4 class="h4">Heading 4</h4>
<p class="text-base">Body text</p>
<p class="text-sm">Small text</p>
<p class="text-xs">Extra small</p>

🌓 Dark Mode

<script>
  import { mode, toggleMode } from '@skeletonlabs/skeleton-svelte'
</script>
 
<!-- Current mode indicator -->
<p>Current: {$mode}</p>
 
<!-- Toggle button -->
<button
  class="btn preset-tonal"
  onclick={toggleMode}
>
  {#if $mode === 'light'}
    <MoonIcon class="size-4" />
  {:else}
    <SunIcon class="size-4" />
  {/if}
</button>
 
<!-- Manual theme class -->
<div class="dark">
  <!-- Always dark in this container -->
</div>

🔥 Common Patterns

Toast Notifications

<script>
  import { Toast, createToaster } from '@skeletonlabs/skeleton-svelte'
 
  const toaster = createToaster()
 
  function showToast() {
    toaster.success({
      title: 'Success!',
      description: 'Your changes have been saved.'
    })
  }
</script>
 
<button class="btn preset-filled" onclick={showToast}>
  Show Toast
</button>
 
<Toast.Group {toaster} />

Tabs

<script>
  import { Tabs } from '@skeletonlabs/skeleton-svelte'
 
  let active = $state('account')
</script>
 
<Tabs value={active} onValueChange={(e) => active = e.value}>
  <Tabs.List classes="bg-surface-100-900/50 p-1 rounded-lg">
    <Tabs.Trigger value="account" classes="px-4 py-2 rounded-md">
      Account
    </Tabs.Trigger>
    <Tabs.Trigger value="settings" classes="px-4 py-2 rounded-md">
      Settings
    </Tabs.Trigger>
  </Tabs.List>
 
  <Tabs.Content value="account">
    Account content
  </Tabs.Content>
  <Tabs.Content value="settings">
    Settings content
  </Tabs.Content>
</Tabs>

Accordion

<script>
  import { Accordion } from '@skeletonlabs/skeleton-svelte'
</script>
 
<Accordion>
  <Accordion.Item>
    {#snippet summary()}Section 1{/snippet}
    {#snippet content()}Content for section 1{/snippet}
  </Accordion.Item>
  <Accordion.Item>
    {#snippet summary()}Section 2{/snippet}
    {#snippet content()}Content for section 2{/snippet}
  </Accordion.Item>
</Accordion>

🎯 Best Practices

  1. Use preset-* classes — consistent with theme system
  2. Prefer semantic colorsprimary, secondary, error, not hex codes
  3. Use adaptive colorssurface-100-900 works in both modes
  4. Combine with Tailwindcard preset-filled p-4 shadow-lg
  5. Leverage snippets — cleaner than slots in Svelte 5

🔗 Resources