Styling in Svelte: a practical Svelte & SvelteKit guide
One of the biggest headaches in modern web development is managing CSS scope and preventing style collisions across components. Svelte solves this elegantly by scoping styles to individual components by default, letting you write CSS without the usual namespace wars. In this guide, we'll walk through Svelte's styling capabilities and show you how to build maintainable, predictable component styles in your applications.
Styles are scoped to the component
Svelte's most powerful feature for styling is automatic scope isolation. When you write a <style> block in a component, those styles apply only to that component's markup. This means you can use generic selectors like button or .card without worrying about accidentally styling buttons in other components.
Let's look at a practical example. Imagine we're building an e-commerce checkout flow with multiple button components.
<script>
export let variant = 'primary';
export let disabled = false;
function handleClick() {
// checkout logic
}
</script>
<button {disabled} on:click={handleClick}>
Complete Purchase
</button>
<style>
button {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
background-color: #007bff;
color: white;
transition: background-color 0.2s;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>This button rule only applies to buttons within this component. If you use this component alongside another component that also has button styles, there's no conflict. Svelte automatically adds a scoped attribute (like svelte-1a2b3c4d) to both the styles and the elements, ensuring isolation.
This approach eliminates the need for BEM naming conventions or CSS-in-JS solutions for basic component styling. You write plain CSS and let Svelte handle the namespace problem.
class: directives for toggling
Dynamic classes are essential for conditional styling. Svelte provides the class: directive to toggle classes based on component state. This is cleaner and more readable than string concatenation.
Consider a notification component that needs different styles based on its type (success, error, warning, info).
<script>
export let type = 'info';
export let message = '';
export let dismissible = true;
let visible = true;
</script>
{#if visible}
<div class="notification" class:success={type === 'success'} class:error={type === 'error'} class:warning={type === 'warning'}>
<p>{message}</p>
{#if dismissible}
<button on:click={() => (visible = false)}>×</button>
{/if}
</div>
{/if}
<style>
.notification {
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.notification.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.notification.error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.notification.warning {
background-color: #fff3cd;
border: 1px solid #ffeeba;
color: #856404;
}
</style>The class: directive syntax is straightforward: class:classname={condition}. When the condition is true, the class is added; when false, it's removed. If the class name matches a variable, you can use the shorthand: class:active is equivalent to class:active={active}.
This approach is superior to building class strings because it's more explicit and easier to track which conditions apply which styles. It also performs better since Svelte only updates the DOM when the condition changes.
style: directives for dynamic values
For inline styles that depend on component state or props, use the style: directive. This is particularly useful for layout-dependent values like positioning, sizing, or colors that come from data.
Let's build a progress indicator for a SaaS onboarding flow.
<script>
export let current = 0;
export let total = 5;
$: percentage = (current / total) * 100;
$: progressColor = percentage < 33 ? '#ff6b6b' : percentage < 66 ? '#ffd93d' : '#6bcf7f';
</script>
<div class="container">
<div class="label">
Step {current} of {total}
</div>
<div class="progress-bar">
<div class="progress-fill" style:width="{percentage}%" style:background-color={progressColor}></div>
</div>
</div>
<style>
.container {
width: 100%;
margin: 20px 0;
}
.label {
font-size: 14px;
margin-bottom: 8px;
color: #666;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #e0e0e0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
transition: width 0.3s ease, background-color 0.3s ease;
}
</style>The style: directive follows the pattern style:property-name={value}. Multiple style directives can be chained on the same element. Notice how we use reactive variables (the $: syntax) to compute both the percentage and the color, then apply them dynamically. This keeps your styling logic separate from markup and makes it testable.
The benefit here is that you avoid building style strings manually or relying on class names for every possible value. When a value is truly dynamic—like an API response determining a color or a user-configured dimension—style: directives are the right tool.
Tailwind integrates easily
Many teams prefer utility-first CSS frameworks like Tailwind. Svelte works seamlessly with Tailwind because Tailwind classes respect component scope just like regular CSS.
To set up Tailwind in a SvelteKit project, install the dependencies and configure them in your build process.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -pThen update your tailwind.config.js to include Svelte files:
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {},
},
plugins: [],
};Now you can use Tailwind classes in your components. Here's a card component built with Tailwind:
<script>
export let title = '';
export let description = '';
export let highlighted = false;
</script>
<div class="card" class:highlighted>
<h3 class="card-title">{title}</h3>
<p class="card-description">{description}</p>
<slot />
</div>
<style>
.card {
@apply rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow;
}
.card:hover {
@apply shadow-md;
}
.card.highlighted {
@apply border-blue-500 bg-blue-50;
}
.card-title {
@apply mb-2 text-lg font-semibold text-gray-900;
}
.card-description {
@apply text-sm text-gray-600;
}
</style>We use the @apply directive to compose Tailwind classes into semantic class names. This keeps your component markup clean while leveraging Tailwind's utility system. You can also use Tailwind classes directly in markup if you prefer, though semantic classes often scale better for complex components.
Svelte's scoping still applies with Tailwind. Classes defined with @apply stay within the component, and Tailwind utilities work exactly as expected. This gives you the best of both worlds: component isolation and utility-first styling.
Common mistakes
Using global selectors in scoped styles: Beginners sometimes expect selectors like * or body to work only within a component. They don't. If you need truly global styles, create a separate stylesheet or use a <style global> block—but use global styles sparingly.
Forgetting that child components have their own scope: If Component A includes Component B and you want to style Component B's elements from Component A, you can't do it with scoped selectors. Instead, pass styling concerns down through props or use CSS variables as a bridge between scopes.
Over-relying on inline styles: While style: directives are powerful, don't use them for everything. If a style depends on state that never changes, put it in the <style> block. Reserve style: directives for truly dynamic values that react to user input or data changes.
Mixing scoped and global CSS inconsistently: Pick a strategy and stick with it. If you're using Tailwind everywhere, don't suddenly switch to scoped utility classes for one component. Consistency makes code easier to maintain across the team.
Key takeaways
- Scoped styles are the default in Svelte—no configuration needed. Write CSS like you normally would and let Svelte handle namespace isolation automatically.
- Use
class:directives to toggle CSS classes based on component state. This is clearer and more performant than building class strings. - Use
style:directives for truly dynamic inline styles—colors from APIs, user-configured sizes, or computed dimensions. - Tailwind integrates seamlessly with Svelte. Install it, configure the content paths, and use classes normally. Scoping still applies.
- Combine scoped styles, class directives, style directives, and utilities to build maintainable, predictable component styling. Each tool has its place.
- Avoid global styles in scoped blocks, respect component boundaries, and keep styling logic close to the markup that uses it.






Comments
Loading comments…