teta.so
SvelteKitTailwind CSSStylingTutorial

SvelteKit + Tailwind CSS: Setup and Best Practices

AI Generated

SvelteKit + Tailwind CSS: Setup and Best Practices

Tailwind CSS has become the go-to styling solution for modern web applications, and for good reason. Its utility-first approach pairs exceptionally well with Svelte's component model. Instead of writing custom CSS classes and managing stylesheets, you compose styles directly in your markup using small, single-purpose utility classes.

With Tailwind v4, the setup has become even simpler. Gone is the sprawling tailwind.config.js file — configuration now lives in your CSS using the new @theme directive. This tutorial covers the complete setup for SvelteKit with Tailwind v4, along with practical patterns and best practices learned from real-world projects.

Prerequisites

  • A SvelteKit project (create one with npx sv create my-app)
  • Node.js 18 or later
  • Basic familiarity with CSS

Step 1: Install Tailwind CSS v4

Tailwind v4 introduces a dramatically simplified installation. There is no PostCSS plugin, no config file, and no content paths to configure:

npm install tailwindcss @tailwindcss/vite

Update your vite.config.ts to include the Tailwind plugin:

import { sveltekit } from '@sveltejs/kit/vite';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    tailwindcss(),
    sveltekit()
  ]
});

Create your main CSS file at src/app.css:

@import "tailwindcss";

That single line imports all of Tailwind's layers (base, components, and utilities). Import this file in your root layout at src/routes/+layout.svelte:

<script>
  import '../app.css';

  let { children } = $props();
</script>

{@render children()}

Start your dev server to verify everything works:

npm run dev

Step 2: Configure Your Theme

Tailwind v4 replaces the JavaScript config file with CSS-native @theme directives. Customize your design tokens directly in src/app.css:

@import "tailwindcss";

@theme {
  /* Colors */
  --color-brand: #0A84FF;
  --color-brand-light: #4DA3FF;
  --color-brand-dark: #0066CC;
  --color-accent: #B25AF9;
  --color-surface: #faf9f5;
  --color-surface-dark: rgb(20, 20, 20);

  /* Fonts */
  --font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, monospace;

  /* Custom spacing */
  --spacing-18: 4.5rem;
  --spacing-88: 22rem;

  /* Border radius */
  --radius-xl: 1rem;
  --radius-2xl: 1.5rem;

  /* Shadows */
  --shadow-soft: 0 2px 8px rgba(0, 0, 0, 0.06);
  --shadow-card: 0 4px 16px rgba(0, 0, 0, 0.08);
}

These tokens automatically become available as utilities. --color-brand generates bg-brand, text-brand, border-brand, and all other color utilities.

Step 3: Build Reusable Component Patterns

Svelte components and Tailwind utilities are a natural fit. Here are practical patterns for common UI elements.

Button Component (src/lib/components/Button.svelte):

<script lang="ts">
  import type { Snippet } from 'svelte';
  import type { HTMLButtonAttributes } from 'svelte/elements';

  interface Props extends HTMLButtonAttributes {
    variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
    size?: 'sm' | 'md' | 'lg';
    children: Snippet;
  }

  let {
    variant = 'primary',
    size = 'md',
    children,
    class: className = '',
    ...rest
  }: Props = $props();

  const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand disabled:opacity-50 disabled:pointer-events-none';

  const variants: Record<string, string> = {
    primary: 'bg-brand text-white hover:bg-brand-dark',
    secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
    ghost: 'text-gray-700 hover:bg-gray-100',
    danger: 'bg-red-600 text-white hover:bg-red-700'
  };

  const sizes: Record<string, string> = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-sm',
    lg: 'px-6 py-3 text-base'
  };
</script>

<button
  class="{baseClasses} {variants[variant]} {sizes[size]} {className}"
  {...rest}
>
  {@render children()}
</button>

Card Component (src/lib/components/Card.svelte):

<script lang="ts">
  import type { Snippet } from 'svelte';

  interface Props {
    children: Snippet;
    padding?: 'none' | 'sm' | 'md' | 'lg';
    class?: string;
  }

  let {
    children,
    padding = 'md',
    class: className = ''
  }: Props = $props();

  const paddings: Record<string, string> = {
    none: '',
    sm: 'p-4',
    md: 'p-6',
    lg: 'p-8'
  };
</script>

<div class="bg-white rounded-xl border border-gray-200 shadow-soft {paddings[padding]} {className}">
  {@render children()}
</div>

Use these components naturally in your pages:

<script>
  import Button from '$lib/components/Button.svelte';
  import Card from '$lib/components/Card.svelte';
</script>

<Card>
  <h2 class="text-xl font-semibold mb-2">Welcome</h2>
  <p class="text-gray-600 mb-4">Get started with your project.</p>
  <div class="flex gap-3">
    <Button>Get Started</Button>
    <Button variant="secondary">Learn More</Button>
  </div>
</Card>

Step 4: Implement Dark Mode

Tailwind's dark mode support works seamlessly with SvelteKit. Use the class strategy for manual toggle control or the media strategy for system preference detection.

For a toggle-based approach, create a theme store at src/lib/stores/theme.svelte.ts:

import { browser } from '$app/environment';

function createTheme() {
  let current = $state<'light' | 'dark'>('light');

  if (browser) {
    const saved = localStorage.getItem('theme');
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    current = (saved as 'light' | 'dark') ?? (prefersDark ? 'dark' : 'light');
    document.documentElement.classList.toggle('dark', current === 'dark');
  }

  return {
    get current() { return current; },
    toggle() {
      current = current === 'light' ? 'dark' : 'light';
      if (browser) {
        document.documentElement.classList.toggle('dark', current === 'dark');
        localStorage.setItem('theme', current);
      }
    }
  };
}

export const theme = createTheme();

Enable the class-based dark mode in your CSS:

@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

Now use dark variants throughout your components:

<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen">
  <header class="border-b border-gray-200 dark:border-gray-800 p-4">
    <h1 class="text-xl font-bold">My App</h1>
  </header>

  <main class="p-6">
    <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
      <p class="text-gray-600 dark:text-gray-400">
        Content adapts to the theme automatically.
      </p>
    </div>
  </main>
</div>

Add a theme toggle button:

<script>
  import { theme } from '$lib/stores/theme.svelte';
</script>

<button
  onclick={() => theme.toggle()}
  class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
  aria-label="Toggle theme"
>
  {theme.current === 'light' ? 'Dark' : 'Light'} Mode
</button>

Step 5: Responsive Design Patterns

Tailwind's responsive prefixes make mobile-first design straightforward. Here is a responsive layout pattern:

<!-- Responsive grid that adapts from 1 to 3 columns -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  {#each items as item}
    <div class="bg-white rounded-lg p-6 shadow-soft">
      <h3 class="font-semibold text-lg">{item.title}</h3>
      <p class="text-gray-600 mt-2">{item.description}</p>
    </div>
  {/each}
</div>

<!-- Responsive navigation -->
<nav class="flex flex-col sm:flex-row sm:items-center gap-4 p-4">
  <a href="/" class="text-lg font-bold">Logo</a>
  <div class="flex flex-col sm:flex-row gap-2 sm:gap-6 sm:ml-auto">
    <a href="/features" class="hover:text-brand">Features</a>
    <a href="/pricing" class="hover:text-brand">Pricing</a>
    <a href="/docs" class="hover:text-brand">Docs</a>
  </div>
</nav>

<!-- Responsive text sizing -->
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold tracking-tight">
  Build faster with SvelteKit
</h1>

Step 6: Use @apply Sparingly

The @apply directive lets you extract utility patterns into custom classes. Use it when you have truly repeated patterns that are not practical as Svelte components:

/* src/app.css */
@import "tailwindcss";

@layer components {
  .prose-content h2 {
    @apply text-2xl font-bold mt-8 mb-4;
  }

  .prose-content h3 {
    @apply text-xl font-semibold mt-6 mb-3;
  }

  .prose-content p {
    @apply text-gray-700 dark:text-gray-300 leading-relaxed mb-4;
  }

  .prose-content code {
    @apply bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm font-mono;
  }

  .prose-content pre {
    @apply bg-gray-900 text-gray-100 p-4 rounded-lg overflow-x-auto mb-4;
  }
}

When to use @apply:

  • Markdown/CMS content styling where you cannot add classes to elements
  • Third-party library overrides
  • Repeated patterns across many non-component elements

When to avoid @apply:

  • Regular components (use Svelte components instead)
  • One-off styles (just use utilities directly)
  • Complex conditional styles (use Svelte's class directive)

Step 7: Optimize for Production

Tailwind v4 automatically tree-shakes unused styles in production builds. Your final CSS bundle only includes the utilities you actually use.

For additional optimization, consider these patterns:

Avoid dynamic class names that cannot be detected:

<!-- Bad: Tailwind cannot detect these classes -->
<div class="bg-{color}-500">...</div>

<!-- Good: Use complete class names -->
<div class={color === 'blue' ? 'bg-blue-500' : 'bg-red-500'}>...</div>

<!-- Good: Map values to full class names -->
<script>
  const colorMap: Record<string, string> = {
    info: 'bg-blue-100 text-blue-800',
    success: 'bg-green-100 text-green-800',
    warning: 'bg-yellow-100 text-yellow-800',
    error: 'bg-red-100 text-red-800'
  };
</script>
<div class={colorMap[status]}>...</div>

Use Svelte's built-in scoped styles for one-off animations:

<div class="bg-white rounded-lg p-6 animate-fade-in">
  <p>This fades in smoothly.</p>
</div>

<style>
  .animate-fade-in {
    animation: fadeIn 0.3s ease-out;
  }

  @keyframes fadeIn {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: translateY(0); }
  }
</style>

Step 8: Organize Your Styles

For large projects, keep your styles organized:

src/
  app.css              # Tailwind imports, theme, global styles
  lib/
    components/
      Button.svelte    # Self-contained with Tailwind utilities
      Card.svelte
      Input.svelte
    styles/
      prose.css        # @apply patterns for rich content
      animations.css   # Shared animations

Import additional CSS files in app.css:

@import "tailwindcss";
@import "./lib/styles/prose.css";
@import "./lib/styles/animations.css";

@theme {
  /* your theme tokens */
}

Common Pitfalls and Solutions

Specificity issues with Svelte scoped styles: Svelte adds scoping attributes to component styles, which can conflict with Tailwind. Use Tailwind utilities in the class attribute rather than mixing with <style> blocks.

Fonts not loading: Import your fonts in app.css before the Tailwind import or in your app.html:

@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap');
@import "tailwindcss";

Transition jank in dark mode: Add a transition to your body element to prevent flash of unstyled content:

html {
  transition: background-color 0.2s ease, color 0.2s ease;
}

Summary

You now have a fully configured SvelteKit + Tailwind CSS v4 setup with:

  • Simple Vite plugin installation (no PostCSS)
  • CSS-native theme configuration with @theme
  • Reusable Svelte component patterns
  • Dark mode with toggle and system preference support
  • Responsive design patterns
  • Production optimization strategies
  • Organized project structure

This combination delivers an exceptional developer experience. Tailwind's utility classes eliminate context switching between markup and stylesheets, while Svelte's component model provides natural boundaries for extracting reusable patterns. Build fast, ship clean.

Source: Teta Engineering

This content was generated with AI from public sources. It represents analysis and commentary, not original journalism.

Build your next app with AI

Try Teta — create sites and apps with AI agents.

Get started free