Admin UI with Tailwind v4 + Skeleton + Svelte 5
Build modern, responsive admin dashboards using Tailwind CSS v4, Skeleton UI components, and Svelte 5 with runes.
Gotchas
Svelte 5 Runes:
- Use
$state()for reactive variables, notlet count = 0with reactivity - Use
$derived()for computed values, not$:reactive statements - Use
$effect()for side effects, notonMountfor most cases - Props use
$props()function, notexport let
Tailwind v4 Differences:
- No
tailwind.config.jsneeded — configuration via CSS@themedirective - Import:
@import 'tailwindcss'not@tailwind base; @tailwind components; @tailwind utilities - Use
@tailwindcss/viteplugin, not@tailwindcss/postcss
Skeleton UI:
- Set
data-theme="cerberus"on<html>element for theme activation - Color tokens are dual-mode:
bg-surface-50-950(light-dark),text-surface-950-50 - Import Skeleton CSS after Tailwind:
@import '@skeletonlabs/skeleton' - Skeleton v4 uses Zag.js for components — check imports from
@skeletonlabs/skeleton-svelte
Common Mistakes:
- Don't use
class="btn btn-primary"— Skeleton usesclass="btn variant-filled-primary" - Don't forget
data-themeattribute — components won't render correctly without it - Don't mix Svelte 4 and 5 syntax in the same file
Quick Start
CDN Setup (for prototyping)
<!doctype html>
<html lang="en" data-theme="cerberus">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Admin Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@skeletonlabs/skeleton@latest/themes/cerberus.css" />
</head>
<body class="bg-surface-50-950">
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold text-surface-950-50">Dashboard</h1>
</div>
</body>
</html>
Full Project Setup
See installation guide for complete Vite + Svelte 5 + Tailwind v4 + Skeleton setup.
Core Layout Pattern
Admin dashboards typically use a three-region layout: sidebar, header, main content.
<script>
let sidebarOpen = $state(true);
</script>
<div class="grid min-h-screen grid-cols-[auto_1fr]">
<aside class="bg-surface-100-900 w-64 border-r border-surface-200-800">
<nav class="p-4">
<a href="/" class="block p-2 hover:bg-surface-200-800 rounded">Dashboard</a>
<a href="/users" class="block p-2 hover:bg-surface-200-800 rounded">Users</a>
<a href="/settings" class="block p-2 hover:bg-surface-200-800 rounded">Settings</a>
</nav>
</aside>
<div class="grid grid-rows-[auto_1fr]">
<header class="bg-surface-50-950 border-b border-surface-200-800 p-4 sticky top-0 z-10">
<button => sidebarOpen = !sidebarOpen}>Menu</button>
</header>
<main class="p-6">
<slot />
</main>
</div>
</div>
For responsive layouts with collapsible sidebar, see layouts reference.
Data Tables
Basic admin table with Skeleton styling:
<script>
import { Table } from '@skeletonlabs/skeleton-svelte';
let users = $state([
{ id: 1, name: 'John Doe', email: 'john@example.com', role: 'Admin' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'User' },
]);
</script>
<div class="overflow-x-auto rounded-lg border border-surface-200-800">
<table class="w-full">
<thead class="bg-surface-100-900">
<tr>
<th class="p-3 text-left text-surface-950-50">Name</th>
<th class="p-3 text-left text-surface-950-50">Email</th>
<th class="p-3 text-left text-surface-950-50">Role</th>
<th class="p-3 text-left text-surface-950-50">Actions</th>
</tr>
</thead>
<tbody>
{#each users as user}
<tr class="border-t border-surface-200-800 hover:bg-surface-100-900">
<td class="p-3">{user.name}</td>
<td class="p-3">{user.email}</td>
<td class="p-3">
<span class="badge variant-filled">{user.role}</span>
</td>
<td class="p-3">
<button class="btn btn-sm variant-ghost">Edit</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
For pagination, sorting, and filtering, see data-tables reference.
Forms
Basic Form Pattern
<script>
let formData = $state({
email: '',
password: '',
remember: false
});
async function handleSubmit() {
// Handle form submission
}
</script>
<form class="space-y-4">
<div>
<label for="email" class="label">Email</label>
<input
type="email"
id="email"
bind:value={formData.email}
class="input variant-form"
placeholder="you@example.com"
/>
</div>
<div>
<label for="password" class="label">Password</label>
<input
type="password"
id="password"
bind:value={formData.password}
class="input variant-form"
/>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="remember" bind:checked={formData.remember} />
<label for="remember" class="label">Remember me</label>
</div>
<button type="submit" class="btn variant-filled">Sign In</button>
</form>
For validation, file uploads, and advanced form patterns, see forms reference.
Navigation
Sidebar Navigation
<script>
import { page } from '$app/stores';
const navItems = [
{ href: '/', label: 'Dashboard', icon: 'dashboard' },
{ href: '/users', label: 'Users', icon: 'users' },
{ href: '/analytics', label: 'Analytics', icon: 'chart' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
];
</script>
<nav class="flex flex-col gap-1">
{#each navItems as item}
<a
href={item.href}
class="flex items-center gap-3 p-3 rounded-lg transition-colors
{$page.url.pathname === item.href
? 'bg-primary-500 text-white'
: 'hover:bg-surface-200-800'}"
>
<span class="w-5 h-5">{item.icon}</span>
<span>{item.label}</span>
</a>
{/each}
</nav>
Breadcrumbs
<nav aria-label="Breadcrumb">
<ol class="flex items-center gap-2 text-sm">
<li><a href="/" class="hover:text-primary-500">Home</a></li>
<li class="text-surface-500">/</li>
<li><a href="/users" class="hover:text-primary-500">Users</a></li>
<li class="text-surface-500">/</li>
<li class="text-surface-950-50 font-medium">Edit User</li>
</ol>
</nav>
For complete navigation patterns, see navigation reference.
Common Components
Cards
<div class="card bg-surface-50-950 border border-surface-200-800 p-4 rounded-xl">
<h3 class="text-lg font-semibold text-surface-950-50">Card Title</h3>
<p class="text-surface-500 mt-2">Card description text here.</p>
</div>
Badges
<span class="badge variant-filled">Default</span>
<span class="badge variant-filled-primary">Primary</span>
<span class="badge variant-filled-secondary">Secondary</span>
<span class="badge variant-filled-success">Success</span>
<span class="badge variant-filled-warning">Warning</span>
<span class="badge variant-filled-error">Error</span>
Buttons
<button class="btn variant-filled">Filled</button>
<button class="btn variant-outline">Outline</button>
<button class="btn variant-ghost">Ghost</button>
<button class="btn variant-filled-primary">Primary</button>
<button class="btn variant-filled-error">Destructive</button>
Dialogs
<script>
import { Dialog } from '@skeletonlabs/skeleton-svelte';
let open = $state(false);
</script>
<button class="btn" => open = true}>Open Dialog</button>
<Dialog bind:open>
<div class="p-6 bg-surface-50-950 rounded-xl">
<h2 class="text-xl font-bold">Dialog Title</h2>
<p class="mt-2 text-surface-500">Dialog content here.</p>
<div class="flex justify-end gap-2 mt-4">
<button class="btn variant-ghost" => open = false}>Cancel</button>
<button class="btn variant-filled">Confirm</button>
</div>
</div>
</Dialog>
For complete component reference, see components reference.
Toast Notifications
<script>
import { toast } from '@skeletonlabs/skeleton-svelte';
function showSuccess() {
toast.success({ title: 'Success', body: 'Operation completed!' });
}
function showError() {
toast.error({ title: 'Error', body: 'Something went wrong.' });
}
</script>
Responsive Design
Breakpoints
| Breakpoint | Min Width | Usage |
|---|---|---|
sm |
640px | sm:block |
md |
768px | md:grid-cols-2 |
lg |
1024px | lg:flex |
xl |
1280px | xl:w-64 |
2xl |
1536px | 2xl:container |
Mobile-First Pattern
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="card">Card 1</div>
<div class="card">Card 2</div>
<div class="card">Card 3</div>
<div class="card">Card 4</div>
</div>
Accessibility
Semantic HTML
Always use semantic elements:
<header><!-- Logo, navigation --></header>
<aside><!-- Sidebar navigation --></aside>
<main><!-- Primary content --></main>
<nav><!-- Navigation menus --></nav>
<footer><!-- Footer content --></footer>
ARIA Labels
<button aria-label="Close menu">
<svg aria-hidden="true">...</svg>
</button>
<nav aria-label="Main navigation">
<!-- nav items -->
</nav>
Focus Management
<script>
import { tick } from 'svelte';
async function openModal() {
modalOpen = true;
await tick();
document.getElementById('first-focusable')?.focus();
}
</script>
Color Tokens
Skeleton provides semantic color tokens that adapt to dark/light mode:
| Token | Light Mode | Dark Mode |
|---|---|---|
bg-surface-50-950 |
Light gray | Near black |
bg-surface-100-900 |
Lighter | Darker |
text-surface-950-50 |
Dark text | Light text |
border-surface-200-800 |
Light border | Dark border |
Use the dual-token pattern for automatic theme adaptation:
<div class="bg-surface-50-950 text-surface-950-50 border-surface-200-800">
Adapts to theme automatically
</div>
Reference Files
Load these files on demand based on the task:
- Installation — Read when setting up a new project or encountering build/config errors
- Layouts — Read when building page structure, sidebar layouts, or responsive grids
- Data Tables — Read when implementing tables with sorting, pagination, or search
- Forms — Read when building forms with validation, file uploads, or multi-step flows
- Navigation — Read when implementing sidebar menus, breadcrumbs, or app bars
- Auth Pages — Read when building login, register, or password reset pages
- Components — Read when using Skeleton badges, buttons, cards, dialogs, or toasts
- Templates — Read when you need a complete page template (dashboard, users list, settings)