Dynamic Styling with the class Attribute
Every interactive interface needs dynamic styling. Think about the experiences you encounter daily: a navigation link that highlights when you’re on that page, a button that dims while a form is submitting, an input field that turns red when validation fails, or a theme toggle that switches between light and dark modes. All of these patterns share a common need—adding and removing CSS classes based on your application’s current state.
If you’ve worked with vanilla JavaScript, you know this can get messy quickly. You end up with classList.add() and classList.remove() calls scattered throughout your code, trying to keep the DOM in sync with your data. Miss one update, and your UI falls out of sync. Svelte eliminates this complexity entirely by letting you describe what classes should be applied when — and the framework handles the rest.
Svelte 5 provides two approaches to dynamic class management. The first is the enhanced class attribute, which gained powerful object and array support in Svelte 5.16. This is the modern, recommended approach that handles everything from simple toggles to complex multi-class logic. The second is the class: directive, which was the original way to conditionally apply classes in earlier Svelte versions.
Important note: The class: directive still works perfectly fine, but it’s now considered legacy. You’ll encounter it in older tutorials, blog posts, and codebases, so it’s worth understanding. However, for any new code you write, the object/array syntax in the class attribute is more powerful, more composable, and the officially recommended approach.
This tutorial will guide you through both approaches comprehensively. We’ll start with the fundamentals, build up to complex real-world patterns, and troubleshoot the most common issues developers encounter. By the end, you’ll have complete confidence in managing CSS classes dynamically in any Svelte 5 application.
The Problem: Why Dynamic Classes Matter
Before we dive into Svelte’s syntax, let’s understand why dynamic class management is such a common challenge and why Svelte’s approach is so much better than what you might be used to.
The Traditional JavaScript Approach
Imagine you’re building a submit button that needs to show different visual states: a “ready” state when the user can click it, a “loading” state while the form is submitting, and a “disabled” state when the form is invalid. In vanilla JavaScript, you might write something like this:
// Imperative approach - managing classes directly in JavaScript
function updateButtonState(button, isLoading, isDisabled) {
// First, remove all possible state classes to start fresh
button.classList.remove('loading', 'disabled', 'ready')
// Then add the appropriate class based on current state
if (isLoading) {
button.classList.add('loading')
} else if (isDisabled) {
button.classList.add('disabled')
} else {
button.classList.add('ready')
}
}
// You have to remember to call this every time state changes
submitButton.addEventListener('click', async () => {
updateButtonState(submitButton, true, false) // Set loading state
try {
await submitForm()
updateButtonState(submitButton, false, false) // Back to ready
} catch (error) {
updateButtonState(submitButton, false, true) // Show disabled/error
}
}) This approach has several problems that become more painful as your application grows:
Scattered logic: Your class management code lives in JavaScript, completely separate from your HTML. To understand how an element is styled, you have to hunt through multiple files and trace function calls.
Manual synchronization: Every single time your state changes, you must remember to call the update function. Forget one call, and your UI shows the wrong state. This is a constant source of bugs.
Error-prone maintenance: What happens when you add a fourth state? You need to find every place that calls
updateButtonState, update the function signature, and make sure the new class is removed in the cleanup step. Miss any of these, and you’ll have subtle bugs.Poor readability: Looking at the HTML alone, you have no idea what classes might be applied dynamically. The connection between your data and your styling is invisible.
Svelte’s Declarative Solution
Now let’s see how Svelte handles the exact same requirement. Instead of imperatively telling the DOM what to do, you declaratively describe what should be true based on your state:
<script>
let isLoading = $state(false)
let isDisabled = $state(false)
</script>
<button
class={{
loading: isLoading,
disabled: isDisabled && !isLoading,
ready: !isLoading && !isDisabled
}}
>
Submit
</button>
<style>
button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.ready {
background-color: #3b82f6;
color: white;
}
.ready:hover {
background-color: #2563eb;
}
.loading {
background-color: #9ca3af;
color: white;
cursor: wait;
}
.disabled {
background-color: #e5e7eb;
color: #9ca3af;
cursor: not-allowed;
}
</style> Look at how much cleaner this is! The relationship between state and styling is crystal clear, right there in the template where you can see it. When isLoading is true, the button gets the loading class. When both isLoading and isDisabled are false, it gets the ready class. There’s no function to call, no synchronization to manage—Svelte watches your reactive state and updates the DOM automatically whenever anything changes.
This declarative approach means you can focus on what you want, not how to make it happen. Your code becomes shorter, more readable, and far less prone to bugs. Now let’s learn how to use this power effectively.
The class Attribute: Your Foundation
The class attribute in Svelte works exactly like you’d expect from HTML, with some powerful enhancements. Let’s start with the basics and build up to more sophisticated patterns.
Static Classes: Just Like HTML
At its simplest, the class attribute works exactly like regular HTML. If you just need static classes that never change, write them as a plain string:
<div class="container">This works exactly like regular HTML</div>
<button class="btn primary rounded"> Static classes only </button>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.btn {
padding: 0.5rem 1rem;
border: none;
cursor: pointer;
}
.primary {
background-color: #3b82f6;
color: white;
}
.rounded {
border-radius: 8px;
}
</style> Nothing special here—these classes are applied when the component mounts and never change. But of course, that’s not why you’re reading this tutorial. Let’s make things dynamic.
Dynamic Classes with Expressions
When you wrap the class value in curly braces {}, you can use any JavaScript expression. The simplest case is using a variable:
<script>
let currentSize = $state('large')
</script>
<div class={currentSize}>This div has class="large"</div>
<style>
.small {
font-size: 0.875rem;
padding: 0.5rem;
}
.medium {
font-size: 1rem;
padding: 1rem;
}
.large {
font-size: 1.25rem;
padding: 1.5rem;
}
</style> When currentSize is 'large', the div gets class="large". If you later change currentSize to 'small', Svelte automatically updates the DOM to class="small". No manual intervention required.
You can also use more complex expressions, like ternary operators, to choose between two possible classes:
<script>
let isExpanded = $state(false)
</script>
<div class={isExpanded ? 'panel-expanded' : 'panel-collapsed'}>
<p>Panel content goes here</p>
</div>
<button onclick={() => (isExpanded = !isExpanded)}>
{isExpanded ? 'Collapse' : 'Expand'} Panel
</button>
<style>
.panel-expanded {
max-height: 500px;
padding: 1.5rem;
opacity: 1;
transition: all 0.3s ease;
}
.panel-collapsed {
max-height: 0;
padding: 0 1.5rem;
opacity: 0;
overflow: hidden;
transition: all 0.3s ease;
}
button {
margin-top: 1rem;
padding: 0.5rem 1rem;
background: #4b5563;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style> Every time you click the button, isExpanded toggles between true and false, and the div’s class updates accordingly. The ternary operator (condition ? valueIfTrue : valueIfFalse) is perfect when you need to choose between exactly two classes.
Understanding Falsy Values
Before we go further, it’s important to understand how Svelte handles “falsy” values in the class attribute. In JavaScript, certain values are considered “falsy”—they evaluate to false in boolean contexts. These include false, 0, '' (empty string), null, undefined, and NaN.
Svelte handles these differently depending on what they are:
<script>
let nullClass = null
let undefinedClass = undefined
let falseClass = false
let emptyString = ''
let zero = 0
</script>
<!-- null and undefined: the class attribute is omitted entirely -->
<div class={nullClass}>No class attribute at all</div>
<div class={undefinedClass}>No class attribute at all</div>
<!-- Other falsy values get converted to strings (probably not what you want!) -->
<div class={falseClass}>class="false" - oops!</div>
<div class={zero}>class="0" - probably not intended</div>
<div class={emptyString}>class="" - empty but present</div> This behavior exists for historical reasons, but it’s important to be aware of it. If your expression might evaluate to false or another falsy value, you need to handle that case explicitly—or better yet, use the object/array syntax we’ll learn next, which handles falsy values gracefully.
Looking ahead: A future version of Svelte will make all falsy values omit the class attribute entirely, making the behavior more consistent. But for now, null and undefined are your safest options when you want to conditionally omit the attribute.
Object Syntax: Fine-Grained Class Control
Starting with Svelte 5.16, the class attribute gained the ability to accept objects. This is a game-changer for dynamic class management, and it’s the approach you should use for most situations. Under the hood, Svelte uses the popular clsx library to process these objects.
How Object Syntax Works
The basic idea is simple: you pass an object where the keys are class names and the values are booleans (or expressions that evaluate to truthy/falsy values). If the value is truthy, the class is applied. If it’s falsy, the class is not applied.
<script>
let isActive = $state(true)
let isDisabled = $state(false)
let isHighlighted = $state(true)
</script>
<div class={{ active: isActive, disabled: isDisabled, highlighted: isHighlighted }}>
This div will have classes: "active highlighted"
</div>
<style>
div {
padding: 1rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
transition: all 0.2s ease;
}
.active {
border-color: #3b82f6;
background-color: #eff6ff;
}
.disabled {
opacity: 0.5;
pointer-events: none;
background-color: #f3f4f6;
}
.highlighted {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
</style> Let’s break down what’s happening here:
active: isActive— SinceisActiveistrue, the classactiveis applieddisabled: isDisabled— SinceisDisabledisfalse, the classdisabledis NOT appliedhighlighted: isHighlighted— SinceisHighlightedistrue, the classhighlightedis applied
The resulting HTML is <div class="active highlighted">. If you later set isDisabled = true and isHighlighted = false, Svelte automatically updates the DOM to <div class="active disabled">.
This pattern is incredibly powerful because each class is independently controlled by its own condition. You don’t have to think about removing old classes or managing state transitions—you just describe what should be true right now.
JavaScript Property Shorthand
When your variable name matches the class name you want to apply, JavaScript’s property shorthand makes things even cleaner:
<script>
// Variable names match the class names we want
let active = $state(true)
let disabled = $state(false)
let loading = $state(false)
</script>
<!-- Using shorthand syntax -->
<button class={{ active, disabled, loading }}>
<!-- Equivalent to: class={{ active: active, disabled: disabled, loading: loading }} -->
Click me
</button>
<style>
button {
padding: 0.75rem 1.5rem;
border: 2px solid #3b82f6;
background: white;
color: #3b82f6;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.active {
background: #3b82f6;
color: white;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: #9ca3af;
color: #9ca3af;
}
.loading {
cursor: wait;
position: relative;
}
.loading::after {
content: '';
position: absolute;
width: 1rem;
height: 1rem;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.6s linear infinite;
right: 0.5rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style> This shorthand is just regular JavaScript object syntax—{ active } is exactly the same as { active: active }. It’s a small thing, but it makes your code cleaner and reduces repetition.
Using Expressions as Values
The values in your object don’t have to be simple boolean variables. You can use any expression that evaluates to a truthy or falsy value:
<script>
let itemCount = $state(5)
let userRole = $state('admin')
let errorMessage = $state('')
</script>
<div
class={{
'has-items': itemCount > 0,
'is-empty': itemCount === 0,
'is-admin': userRole === 'admin',
'is-moderator': userRole === 'moderator',
'has-error': errorMessage.length > 0
}}
>
<p>Items: {itemCount}</p>
<p>Role: {userRole}</p>
</div>
<style>
div {
padding: 1rem;
border-radius: 8px;
border: 2px solid #e5e7eb;
}
.has-items {
background-color: #f0fdf4;
border-color: #86efac;
}
.is-empty {
background-color: #fef2f2;
border-color: #fca5a5;
}
.is-admin {
border-left: 4px solid #8b5cf6;
}
.is-moderator {
border-left: 4px solid #f59e0b;
}
.has-error {
background-color: #fef2f2;
border-color: #ef4444;
}
</style> Each key-value pair is evaluated independently. The class has-items is applied because itemCount > 0 is true (5 > 0). The class is-admin is applied because userRole === 'admin' is true. The class has-error is NOT applied because errorMessage.length > 0 is false (empty string has length 0).
Dynamic Class Names with Computed Keys
Sometimes you need the class name itself to be dynamic, not just whether it’s applied. You can use JavaScript’s computed property syntax (square brackets) to create class names from expressions:
<script>
let theme = $state('dark')
let size = $state('large')
let variant = $state('primary')
</script>
<button
class={{
[`theme-${theme}`]: true,
[`size-${size}`]: true,
[`variant-${variant}`]: true
}}
>
Dynamically named classes
</button>
<!-- Result: class="theme-dark size-large variant-primary" -->
<style>
button {
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
}
/* Theme classes */
.theme-light {
background-color: #f9fafb;
color: #1f2937;
}
.theme-dark {
background-color: #1f2937;
color: #f9fafb;
}
/* Size classes */
.size-small {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.size-medium {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.size-large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
/* Variant classes */
.variant-primary {
background-color: #3b82f6;
color: white;
}
.variant-secondary {
background-color: #6b7280;
color: white;
}
.variant-danger {
background-color: #ef4444;
color: white;
}
</style> The square brackets tell JavaScript to evaluate the expression inside and use the result as the property key. So [`theme-${theme}`] becomes 'theme-dark' when theme is 'dark'. The value true means “always apply this class.”
This is particularly useful when you’re working with a design system that uses naming conventions like variant-primary, size-lg, or theme-dark.
Applying Multiple Classes with One Condition
One of the most powerful features of object syntax is the ability to toggle multiple classes with a single condition. Just put all the class names in the key, separated by spaces:
<script>
let isError = $state(false)
let isSuccess = $state(false)
let isLoading = $state(true)
</script>
<div
class={{
'loading pulse': isLoading,
'error shake': isError,
'success fade-in': isSuccess
}}
>
{#if isLoading}
Loading...
{:else if isError}
Something went wrong!
{:else if isSuccess}
Operation completed successfully!
{:else}
Ready
{/if}
</div>
<style>
div {
padding: 1rem;
border-radius: 8px;
text-align: center;
font-weight: 500;
}
/* Loading state styles */
.loading {
background-color: #f3f4f6;
color: #6b7280;
}
.pulse {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Error state styles */
.error {
background-color: #fef2f2;
color: #dc2626;
border: 1px solid #fca5a5;
}
.shake {
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
25% {
transform: translateX(-5px);
}
75% {
transform: translateX(5px);
}
}
/* Success state styles */
.success {
background-color: #f0fdf4;
color: #16a34a;
border: 1px solid #86efac;
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style> When isLoading is true, both classes (loading and pulse) are applied together. When it becomes false, both are removed together. This makes it easy to group related styles—the visual appearance and the animation go hand in hand.
Think about it: in the old way, you’d need separate logic for each class. With object syntax, related classes are grouped by their condition, making your intent crystal clear.
Array Syntax: Combining Multiple Class Sources
While object syntax is great for boolean toggles, array syntax shines when you need to combine multiple class sources or use more complex conditional logic. Like objects, arrays are processed by clsx and all falsy values are automatically filtered out.
How Array Syntax Works
With array syntax, you provide an array of values. Each value can be a string (which is always included) or an expression that evaluates to either a string (included) or a falsy value (excluded):
<script>
let isPrimary = $state(true)
let isLarge = $state(false)
let isDisabled = $state(false)
</script>
<button
class={['btn', 'rounded', isPrimary && 'primary', isLarge && 'large', isDisabled && 'disabled']}
>
Click me
</button>
<!-- Result: class="btn rounded primary" -->
<style>
.btn {
padding: 0.5rem 1rem;
border: none;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.rounded {
border-radius: 6px;
}
.primary {
background-color: #3b82f6;
color: white;
}
.primary:hover {
background-color: #2563eb;
}
.large {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
</style> Let’s trace through each element:
'btn'— A plain string, always included'rounded'— Another plain string, always includedisPrimary && 'primary'—isPrimaryistrue, so this evaluates to'primary'(included)isLarge && 'large'—isLargeisfalse, so this evaluates tofalse(excluded)isDisabled && 'disabled'—isDisabledisfalse, so this evaluates tofalse(excluded)
The magic is in how JavaScript’s && operator works: if the left side is falsy, it returns the left side (which gets filtered out). If the left side is truthy, it returns the right side (the class name). This pattern is so common in React and Svelte that it’s worth memorizing.
Conditional Class Groups with Ternary Operators
Sometimes you need to choose between two different class sets, not just include or exclude a single class. Ternary operators work perfectly inside arrays:
<script>
let status = $state('loading')
</script>
<div
class={[
'status-card',
status === 'loading'
? 'loading'
: status === 'error'
? 'error'
: status === 'success'
? 'success'
: 'idle'
]}
>
<span class="status-icon">
{#if status === 'loading'}
⏳
{:else if status === 'error'}
❌
{:else if status === 'success'}
✅
{:else}
💤
{/if}
</span>
<span class="status-text">Status: {status}</span>
</div>
<style>
.status-card {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
border-radius: 8px;
border: 2px solid;
}
.status-icon {
font-size: 1.5rem;
}
.status-text {
font-weight: 500;
text-transform: capitalize;
}
.loading {
background-color: #f3f4f6;
border-color: #d1d5db;
color: #6b7280;
}
.error {
background-color: #fef2f2;
border-color: #fca5a5;
color: #dc2626;
}
.success {
background-color: #f0fdf4;
border-color: #86efac;
color: #16a34a;
}
.idle {
background-color: #f9fafb;
border-color: #e5e7eb;
color: #374151;
}
</style> The base class (status-card) is always applied. Then, depending on the status value, different style classes are added. This creates a clean mapping between state and presentation.
For better readability with multiple states, you might prefer multiple conditions:
<div
class={[
'status-card',
status === 'loading' && 'loading',
status === 'error' && 'error',
status === 'success' && 'success',
status === 'idle' && 'idle'
]}
>
<!-- content -->
</div> Both approaches work; choose whichever is clearer for your specific case. When states are mutually exclusive (only one can be true at a time), the second approach with separate conditions is often more readable.
Nesting Arrays and Objects
One of the most powerful features of clsx (and therefore Svelte’s class handling) is that arrays can contain other arrays and even objects. Everything gets flattened and processed correctly:
<script>
// Base classes that multiple elements might share
const cardBase = ['card', 'rounded', 'shadow']
const interactiveStyles = ['clickable', 'hoverable']
let isSelected = $state(false)
let variant = $state('default')
</script>
<button
type="button"
class={[
cardBase,
interactiveStyles,
{ selected: isSelected, ring: isSelected },
variant === 'featured' && 'featured'
]}
onclick={() => (isSelected = !isSelected)}
aria-pressed={isSelected}
>
<h3>Card Title</h3>
<p>A card that combines multiple class sources. Click to select!</p>
</button>
<style>
button.card {
padding: 1.5rem;
background: white;
border: 1px solid #e5e7eb;
text-align: left;
width: 100%;
font: inherit;
}
.rounded {
border-radius: 12px;
}
.shadow {
box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
}
.clickable {
cursor: pointer;
user-select: none;
}
.hoverable {
transition: all 0.2s ease;
}
.hoverable:hover {
transform: translateY(-2px);
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.selected {
border-color: #3b82f6;
background-color: #eff6ff;
}
.ring {
box-shadow:
0 0 0 3px rgba(59, 130, 246, 0.3),
0 4px 6px -1px rgb(0 0 0 / 0.1);
}
.featured {
border-width: 2px;
border-color: #f59e0b;
}
h3 {
margin: 0 0 0.5rem;
font-size: 1.125rem;
color: #111827;
}
p {
margin: 0;
color: #6b7280;
}
</style> This example demonstrates several concepts working together:
- Reusable class arrays (
cardBase,interactiveStyles) that can be shared across components - An embedded object for toggle-based classes
- A conditional expression for variant-specific classes
Everything gets flattened into a single class string. This composability is incredibly powerful for building design systems where you want to share base styles while allowing customization.
Component Class Composition
When building reusable components, you’ll often want to let consumers add their own classes while preserving the component’s base styling. This is where Svelte 5’s class handling really shines.
The Challenge: Accepting External Classes
Imagine you’re building a Button component. It has its own styles, but you want users to be able to add extra classes for spacing, sizing, or custom styling without modifying the component itself. Here’s how to do it:
<!-- Button.svelte -->
<script>
let { class: className, variant = 'primary', children } = $props()
</script>
<button
class={[
'btn',
variant === 'primary' && 'primary',
variant === 'secondary' && 'secondary',
variant === 'ghost' && 'ghost',
className
]}
>
{@render children?.()}
</button>
<style>
.btn {
padding: 0.625rem 1.25rem;
border: 2px solid transparent;
border-radius: 6px;
font-weight: 500;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s ease;
}
.btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.4);
}
.primary {
background-color: #3b82f6;
color: white;
}
.primary:hover {
background-color: #2563eb;
}
.secondary {
background-color: #e5e7eb;
color: #374151;
}
.secondary:hover {
background-color: #d1d5db;
}
.ghost {
background-color: transparent;
color: #374151;
border-color: #e5e7eb;
}
.ghost:hover {
background-color: #f3f4f6;
}
</style> Notice a few important things:
We rename the prop: We use
class: classNamebecauseclassis a reserved word in JavaScript. This destructures theclassprop into a variable calledclassName.We put
classNamelast: This is intentional! By putting the user’s classes last, we allow them to add additional styling that works alongside our base styles.The component defines its base styles: The button always has
btnclass. These are the non-negotiable base styles.Variant classes are conditional: Depending on the
variantprop, different color/style classes are applied.
Now consumers can use the component like this:
<script>
import Button from './Button.svelte'
</script>
<!-- Basic usage -->
<Button>Click me</Button>
<!-- With custom classes -->
<Button class="extra-margin">With spacing</Button>
<!-- Different variant with custom classes -->
<Button variant="secondary" class="full-width">Secondary button</Button>
<style>
.extra-margin {
margin-top: 1rem;
}
.full-width {
width: 100%;
}
</style> Flexible Input Types
One of the beautiful things about Svelte’s class handling is that consumers can pass classes as strings, objects, or arrays—whatever is most convenient for their use case:
<script>
import Button from './Button.svelte'
let isLoading = $state(false)
let isFullWidth = $state(true)
</script>
<!-- String (simplest) -->
<Button class="custom-class">String classes</Button>
<!-- Object (for conditional classes) -->
<Button class={{ loading: isLoading, 'full-width': isFullWidth }}>Object classes</Button>
<!-- Array (for complex combinations) -->
<Button class={['spaced', isFullWidth && 'full-width', isLoading && 'loading']}>
Array classes
</Button>
<style>
:global(.custom-class) {
border-style: dashed;
}
:global(.loading) {
opacity: 0.7;
cursor: wait;
}
:global(.full-width) {
width: 100%;
}
:global(.spaced) {
margin: 0.5rem;
}
</style> Because everything flows through clsx, all these formats work seamlessly. Your component doesn’t need to do anything special—just include className in your class array, and Svelte handles the rest.
TypeScript Support with ClassValue
If you’re using TypeScript, you’ll want to properly type your class props. Svelte 5.19 introduced the ClassValue type specifically for this purpose:
<script lang="ts">
import type { ClassValue } from 'svelte/elements'
import type { Snippet } from 'svelte'
interface Props {
class?: ClassValue
variant?: 'primary' | 'secondary' | 'ghost'
disabled?: boolean
children?: Snippet
}
let { class: className, variant = 'primary', disabled = false, children }: Props = $props()
</script>
<button class={['btn', `variant-${variant}`, disabled && 'disabled', className]} {disabled}>
{@render children?.()}
</button>
<style>
.btn {
padding: 0.625rem 1.25rem;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
}
.variant-primary {
background-color: #3b82f6;
color: white;
}
.variant-secondary {
background-color: #e5e7eb;
color: #374151;
}
.variant-ghost {
background-color: transparent;
border: 2px solid #e5e7eb;
color: #374151;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
</style> The ClassValue type accepts strings, objects, arrays, and any nested combination thereof—exactly what Svelte’s class handling supports. This gives you full type safety while maintaining complete flexibility.
The class: Directive (Legacy Syntax)
Before Svelte 5.16 added object and array support to the class attribute, the class: directive was the only way to conditionally apply classes. While it still works perfectly fine, it’s now considered legacy syntax. You should use the object/array syntax for new code, but understanding the directive is valuable since you’ll encounter it in older codebases, tutorials, and Stack Overflow answers.
How the Directive Works
The class: directive follows the pattern class:name={condition}, where name is the class to apply and condition is a boolean expression:
<script>
let isActive = $state(true)
let isDisabled = $state(false)
let isLoading = $state(false)
</script>
<button class:active={isActive} class:disabled={isDisabled} class:loading={isLoading}>
Click me
</button>
<!-- When isActive is true: class="active" -->
<style>
button {
padding: 0.75rem 1.5rem;
border: 2px solid #3b82f6;
background: white;
color: #3b82f6;
border-radius: 6px;
cursor: pointer;
}
.active {
background: #3b82f6;
color: white;
}
.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.loading {
cursor: wait;
}
</style> When the condition is truthy, the class is added. When it’s falsy, the class is removed. Simple enough, but notice how each class needs its own directive. Compare this to the object syntax:
<!-- Object syntax: one attribute, all conditions together -->
<button class={{ active: isActive, disabled: isDisabled, loading: isLoading }}> Click me </button>
<!-- Directive syntax: separate directive for each class -->
<button class:active={isActive} class:disabled={isDisabled} class:loading={isLoading}>
Click me
</button> Both produce the same result, but the object syntax is more compact and keeps all the class logic in one place.
Shorthand Syntax
When your variable name exactly matches the class name you want to apply, you can use a shorthand that omits the value:
<script>
let active = $state(true)
let disabled = $state(false)
let highlighted = $state(true)
</script>
<!-- Shorthand: class:name is equivalent to class:name={name} -->
<div class:active class:disabled class:highlighted>Content</div>
<!-- Result: class="active highlighted" -->
<style>
div {
padding: 1rem;
border: 2px solid #e5e7eb;
border-radius: 8px;
}
.active {
border-color: #3b82f6;
}
.disabled {
opacity: 0.5;
}
.highlighted {
background-color: #fef3c7;
}
</style> This only works when the variable name and class name are identical. It’s a nice convenience, but it constrains your variable naming.
Combining Directive with Static Classes
You can use the class: directive alongside a regular class attribute for static classes:
<script>
let isSelected = $state(false)
</script>
<div class="card" class:selected={isSelected} class:highlighted={isSelected}>
A card that might be selected
</div>
<style>
.card {
padding: 1.5rem;
border-radius: 12px;
border: 2px solid #e5e7eb;
background: white;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
}
.selected {
border-color: #3b82f6;
background-color: #eff6ff;
}
.highlighted {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3);
}
</style> The static classes (card) are always present. The directive-controlled classes (selected, highlighted) are added or removed based on isSelected.
Why Object/Array Syntax is Better
The directive syntax has several limitations that the modern syntax solves:
No multi-class conditions: You can’t toggle multiple classes with one directive. Each class needs its own directive.
More verbose: Five conditional classes means five separate directives cluttering your element.
Not composable: You can’t easily combine directive classes with classes passed as props.
Mixes two syntaxes: Using both
class="..."andclass:nameon the same element can be confusing.
The object/array syntax solves all of these issues. Use it for new code, and consider migrating legacy code when you’re making other changes to those components.
Real-World Patterns and Examples
Let’s look at some complete, practical examples that demonstrate how to use dynamic classes in real applications.
1. Navigation with Active State
Almost every application needs navigation that highlights the current page. Here’s a complete, accessible implementation:
<script>
import { page } from '$app/stores'
const navItems = [
{ href: '/', label: 'Home', icon: '🏠' },
{ href: '/products', label: 'Products', icon: '📦' },
{ href: '/about', label: 'About', icon: 'ℹ️' },
{ href: '/contact', label: 'Contact', icon: '✉️' }
]
</script>
<nav aria-label="Main navigation">
<ul class="nav-list">
{#each navItems as item}
{@const isActive = $page.url.pathname === item.href}
<li>
<a
href={item.href}
class={['nav-link', isActive && 'active']}
aria-current={isActive ? 'page' : undefined}
>
<span class="nav-icon" aria-hidden="true">{item.icon}</span>
<span class="nav-label">{item.label}</span>
</a>
</li>
{/each}
</ul>
</nav>
<style>
nav {
background: white;
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.05);
}
.nav-list {
display: flex;
gap: 0.25rem;
list-style: none;
margin: 0;
padding: 0 1rem;
max-width: 1200px;
margin: 0 auto;
}
.nav-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem;
text-decoration: none;
color: #6b7280;
font-weight: 500;
border-bottom: 3px solid transparent;
transition: all 0.2s ease;
}
.nav-link:hover {
color: #374151;
background-color: #f9fafb;
}
.nav-link.active {
color: #2563eb;
border-bottom-color: #2563eb;
background-color: #eff6ff;
}
.nav-icon {
font-size: 1.25rem;
}
.nav-label {
font-size: 0.9375rem;
}
</style> Key points in this example:
- We use
@constto computeisActiveonce per iteration, keeping the template clean - The
aria-current="page"attribute is conditionally applied for accessibility - Active items get a visible bottom border, colored text, and a subtle background
- Inactive items have hover states that preview what the active state looks like
- The icon is marked
aria-hiddensince it’s decorative
2. Form Validation with Visual Feedback
Form inputs need to communicate their validation state clearly. Here’s a complete input component with validation styling:
<script>
let value = $state('')
let touched = $state(false)
let focused = $state(false)
// Validation logic
let isEmpty = $derived(value.trim().length === 0)
let isTooShort = $derived(value.trim().length > 0 && value.trim().length < 3)
let isValid = $derived(value.trim().length >= 3)
// Determine what to show (only after user has interacted)
let showError = $derived(touched && !focused && !isValid)
let showSuccess = $derived(touched && isValid)
// Error message to display
let errorMessage = $derived.by(() => {
if (!touched || focused) return ''
if (isEmpty) return 'This field is required'
if (isTooShort) return 'Must be at least 3 characters'
return ''
})
</script>
<div class="form-field">
<label for="username">Username</label>
<div class="input-wrapper">
<input
id="username"
type="text"
bind:value
onfocus={() => (focused = true)}
onblur={() => {
focused = false
touched = true
}}
class={[
'input',
showError && 'error',
showSuccess && 'success',
!showError && !showSuccess && 'neutral'
]}
aria-invalid={showError}
aria-describedby={showError ? 'username-error' : undefined}
/>
{#if showSuccess}
<span class="success-icon">✓</span>
{/if}
</div>
{#if showError}
<p id="username-error" class="error-message">
<span aria-hidden="true">⚠️</span>
{errorMessage}
</p>
{/if}
{#if !touched}
<p class="hint">Choose a username with at least 3 characters</p>
{/if}
</div>
<style>
.form-field {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
label {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
}
.input-wrapper {
position: relative;
}
.input {
width: 100%;
padding: 0.625rem 1rem;
font-size: 1rem;
border: 2px solid;
border-radius: 8px;
outline: none;
transition: all 0.2s ease;
}
.input.neutral {
border-color: #d1d5db;
background-color: white;
}
.input.neutral:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
}
.input.error {
border-color: #ef4444;
background-color: #fef2f2;
color: #991b1b;
}
.input.error:focus {
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
}
.input.success {
border-color: #22c55e;
background-color: #f0fdf4;
}
.input.success:focus {
box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2);
}
.success-icon {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
color: #22c55e;
font-weight: bold;
}
.error-message {
display: flex;
align-items: center;
gap: 0.375rem;
margin: 0;
font-size: 0.875rem;
color: #dc2626;
}
.hint {
margin: 0;
font-size: 0.875rem;
color: #6b7280;
}
</style> This example shows several important practices:
- Validation only shows after the user has interacted (
touched) - Errors don’t flash while the user is still typing (
!focused) - Three distinct visual states: error (red), success (green), and neutral (gray/blue)
- Proper accessibility with
aria-invalidandaria-describedby - A success checkmark provides positive reinforcement
- Helpful hint text before the user interacts
3. Interactive Card Selection
Cards that can be selected need clear visual feedback. Here’s a pattern for a selectable card grid:
<script>
let plans = $state([
{
id: 'basic',
name: 'Basic',
price: 9,
features: ['5 projects', '10GB storage', 'Email support']
},
{
id: 'pro',
name: 'Professional',
price: 29,
features: ['Unlimited projects', '100GB storage', 'Priority support', 'API access']
},
{
id: 'enterprise',
name: 'Enterprise',
price: 99,
features: [
'Everything in Pro',
'Unlimited storage',
'Dedicated support',
'Custom integrations',
'SLA guarantee'
]
}
])
let selectedPlan = $state('pro')
</script>
<div class="plan-grid">
{#each plans as plan}
{@const isSelected = selectedPlan === plan.id}
<button
class={['plan-card', isSelected && 'selected']}
onclick={() => (selectedPlan = plan.id)}
aria-pressed={isSelected}
>
{#if isSelected}
<span class="selected-badge">Selected</span>
{/if}
<h3 class={['plan-name', isSelected && 'selected']}>{plan.name}</h3>
<p class="plan-price">
<span class={['price-amount', isSelected && 'selected']}>${plan.price}</span>
<span class="price-period">/month</span>
</p>
<ul class="feature-list">
{#each plan.features as feature}
<li class="feature-item">
<span class={['check-icon', isSelected && 'selected']}>✓</span>
{feature}
</li>
{/each}
</ul>
</button>
{/each}
</div>
<style>
.plan-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
padding: 1rem;
}
.plan-card {
position: relative;
padding: 2rem;
background: white;
border: 2px solid #e5e7eb;
border-radius: 16px;
text-align: left;
cursor: pointer;
transition: all 0.2s ease;
}
.plan-card:hover {
border-color: #d1d5db;
box-shadow: 0 10px 25px -5px rgb(0 0 0 / 0.1);
transform: translateY(-2px);
}
.plan-card.selected {
border-color: #3b82f6;
background-color: #eff6ff;
box-shadow:
0 0 0 3px rgba(59, 130, 246, 0.2),
0 10px 25px -5px rgb(0 0 0 / 0.1);
transform: scale(1.02);
}
.selected-badge {
position: absolute;
top: -0.75rem;
right: -0.5rem;
padding: 0.25rem 0.75rem;
background: #3b82f6;
color: white;
font-size: 0.75rem;
font-weight: 600;
border-radius: 9999px;
}
.plan-name {
margin: 0 0 0.5rem;
font-size: 1.25rem;
color: #111827;
}
.plan-name.selected {
color: #1d4ed8;
}
.plan-price {
margin: 0 0 1.5rem;
}
.price-amount {
font-size: 2rem;
font-weight: 700;
color: #111827;
}
.price-amount.selected {
color: #2563eb;
}
.price-period {
font-size: 1rem;
color: #6b7280;
}
.feature-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.625rem;
}
.feature-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9375rem;
color: #4b5563;
}
.check-icon {
color: #9ca3af;
font-weight: bold;
}
.check-icon.selected {
color: #3b82f6;
}
</style> This pattern demonstrates:
- Cards are buttons for proper keyboard accessibility
- Selected state uses color, shadow, and subtle scale for emphasis
- Unselected cards have hover states that hint at interactivity
- The “Selected” badge makes the current selection unmistakable
aria-pressedcommunicates selection state to screen readers
4. Theme Toggle (Light/Dark Mode)
A theme toggle is a common pattern that demonstrates dynamic class management well:
<script>
let isDark = $state(false)
</script>
<div class={['app-container', isDark && 'dark-theme']}>
<header class="header">
<h1>My Application</h1>
<button class="theme-toggle" onclick={() => (isDark = !isDark)}>
<span class="toggle-icon">{isDark ? '☀️' : '🌙'}</span>
<span class="toggle-text">{isDark ? 'Light Mode' : 'Dark Mode'}</span>
</button>
</header>
<main class="content">
<p>
This content adapts to the current theme. The background, text colors, and other elements all
respond to the theme state.
</p>
<div class="card">
<h2>Feature Card</h2>
<p>Cards and other components inherit the theme styling automatically.</p>
</div>
</main>
</div>
<style>
.app-container {
min-height: 100vh;
background-color: #ffffff;
color: #1f2937;
transition: all 0.3s ease;
}
.app-container.dark-theme {
background-color: #111827;
color: #f3f4f6;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
border-bottom: 1px solid #e5e7eb;
}
.dark-theme .header {
border-bottom-color: #374151;
}
h1 {
margin: 0;
font-size: 1.5rem;
}
.theme-toggle {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: #f3f4f6;
color: #374151;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.dark-theme .theme-toggle {
background-color: #374151;
color: #f3f4f6;
}
.theme-toggle:hover {
background-color: #e5e7eb;
}
.dark-theme .theme-toggle:hover {
background-color: #4b5563;
}
.toggle-icon {
font-size: 1.25rem;
}
.toggle-text {
font-size: 0.875rem;
font-weight: 500;
}
.content {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.card {
margin-top: 2rem;
padding: 1.5rem;
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.dark-theme .card {
background-color: #1f2937;
border-color: #374151;
}
.card h2 {
margin: 0 0 0.5rem;
font-size: 1.125rem;
}
.card p {
margin: 0;
color: #6b7280;
}
.dark-theme .card p {
color: #9ca3af;
}
</style> This example shows how a single state variable (isDark) can control styling across an entire page. The parent container gets the dark-theme class, and child elements can use the .dark-theme .element selector pattern to adapt their styling.
Common Struggles and How to Solve Them
Even experienced developers run into issues with dynamic classes. Here are the most common problems and their solutions.
1. String Concatenation Creating Gaps or “false”
When developers first try dynamic classes, they often reach for string concatenation, which leads to problems:
<script>
let isActive = $state(false)
let isLarge = $state(true)
</script>
<!-- AVOID: Extra spaces in the class string -->
<div class="base {isActive ? 'active' : ''} {isLarge ? 'large' : ''}">
<!-- Result: "base large" - notice the double space! -->
</div>
<!-- AVOID: "false" becomes a literal class name -->
<div class={'base ' + (isActive && 'active')}>
<!-- Result: "base false" - oops! -->
</div> The first example leaves an empty string in the middle, creating a double space. The second example is worse: when isActive is false, the && operator returns false, which gets concatenated as the literal string “false”.
The solution is simple: use arrays instead.
<!-- PREFERRED: Arrays filter out falsy values automatically -->
<div class={['base', isActive && 'active', isLarge && 'large']}>
<!-- Result: "base large" - clean! -->
</div>
<!-- PREFERRED: Objects work too -->
<div class={{ base: true, active: isActive, large: isLarge }}>
<!-- Result: "base large" -->
</div> Both arrays and objects handle falsy values gracefully—they’re simply omitted from the final class string.
2. Scoped Styles Not Applying to Dynamic Classes
Svelte scopes your component’s styles by adding unique class attributes. But if a class name only appears dynamically, Svelte might not include it in the scoped styles:
<script>
let isHighlighted = $state(true)
</script>
<div class={{ highlight: isHighlighted }}>This text should have a yellow background...</div>
<style>
.highlight {
background-color: yellow;
}
</style> If Svelte’s compiler doesn’t see .highlight used as a static class anywhere, it might not include the scoped version of that style. This is an edge case, but it can be confusing when it happens.
Solution 1: Use :global() for dynamic classes
<style>
:global(.highlight) {
background-color: yellow;
}
</style> Solution 2: Always include a static reference
Since the class is used in an object/array expression, modern Svelte should detect it. But if you’re having issues, you can ensure the class is detected by including it statically somewhere (even in an always-false condition).
3. TypeScript Complaining About class Prop
TypeScript doesn’t love using class as a prop name since it’s a reserved word:
<script lang="ts">
// TypeScript might complain about this
let { class: className } = $props()
</script> Solution: Use the ClassValue type from svelte/elements
<script lang="ts">
import type { ClassValue } from 'svelte/elements'
interface Props {
class?: ClassValue
// other props...
}
let { class: className, ...rest }: Props = $props()
</script>
<div class={['base-class', className]}>Fully typed!</div> The ClassValue type correctly represents all the possible values that Svelte’s class handling accepts: strings, objects, arrays, and nested combinations.
4. Reactivity Issues with Class Objects
If you’re storing your class conditions in an object variable (rather than inline), you might run into reactivity issues:
<script>
// AVOID: Mutating object properties directly
let classes = { active: false }
function toggle() {
classes.active = !classes.active // Might not trigger update!
}
</script>
<div class={classes}>Content</div> Solution: Use $state for reactive objects
<script>
// PREFERRED: Use $state for the object
let classState = $state({ active: false })
function toggle() {
classState.active = !classState.active // Works with $state!
}
</script>
<div class={{ active: classState.active }}>Content</div>
<!-- Or even simpler, use a boolean state -->
<script>
let isActive = $state(false)
</script>
<div class={{ active: isActive }}>Content</div> Using $state ensures Svelte tracks changes to the object’s properties and updates the DOM accordingly.
Performance Considerations
While Svelte’s class handling is already highly optimized, understanding performance implications helps you write better code.
Extract Complex Conditions to $derived
When you have complex logic determining classes, extract it to $derived rather than computing it inline:
<script>
let items = $state([])
let filter = $state('')
// AVOID: This complex expression runs on every render
// class={{ 'has-results': items.filter(i => i.name.includes(filter)).length > 0 }}
// PREFERRED: This only recalculates when items or filter change
let hasResults = $derived(items.some((i) => i.name.includes(filter)))
</script>
<div class={{ 'has-results': hasResults, 'no-results': !hasResults }}>
{hasResults ? 'Found items!' : 'No items match your filter'}
</div>
<style>
.has-results {
background-color: #f0fdf4;
border-color: #86efac;
}
.no-results {
background-color: #fef2f2;
border-color: #fca5a5;
}
</style> The $derived value is cached and only recalculates when its dependencies change. Inline expressions in the class attribute recalculate on every render, even if the inputs haven’t changed.
Define Static Class Mappings Outside the Template
If you have a fixed set of variants or states, define the class mappings as a constant:
<script>
let variant = $state('primary')
let size = $state('md')
// Define mappings once, outside the reactive scope
const variantClasses = {
primary: 'variant-primary',
secondary: 'variant-secondary',
ghost: 'variant-ghost',
danger: 'variant-danger'
}
const sizeClasses = {
sm: 'size-sm',
md: 'size-md',
lg: 'size-lg'
}
</script>
<button class={['btn', variantClasses[variant], sizeClasses[size]]}> Click me </button>
<style>
.btn {
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
}
.variant-primary {
background-color: #3b82f6;
color: white;
}
.variant-secondary {
background-color: #e5e7eb;
color: #374151;
}
.variant-ghost {
background-color: transparent;
border: 2px solid #e5e7eb;
color: #374151;
}
.variant-danger {
background-color: #ef4444;
color: white;
}
.size-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.size-md {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.size-lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
</style> These constant objects are created once when the component initializes, not on every render. It’s also more maintainable—all your variant definitions are in one place.
When Performance Doesn’t Matter (Most of the Time)
For the vast majority of applications, performance differences between various class-handling approaches are completely negligible. Svelte’s compiler is remarkably efficient, and clsx (which powers object/array processing) is highly optimized.
Focus on:
- Readability: Can you understand this class logic at a glance?
- Maintainability: Is it easy to add a new variant or state?
- Correctness: Does it produce the right classes in all situations?
Only optimize for performance if you’ve measured a problem. Premature optimization of class handling is almost never worth the trade-off in code clarity.
Best Practices Summary
After covering all the details, here are the key practices to remember:
Use object/array syntax for new code. The
class:directive is legacy—there’s no reason to use it in new Svelte 5 projects.Choose the right syntax for the situation. Objects are great for boolean toggles (
class={{ active: isActive }}). Arrays are great for combining multiple sources and conditional groups (class={[baseClasses, variant && variantClass]}).Group related classes under single conditions. When a visual state requires multiple classes, put them all in one string:
'loading pulse': isLoading.Put consumer classes last in component arrays. This allows users to add additional styling:
class={['base-styles', className]}.Type your class props in TypeScript. Import
ClassValuefromsvelte/elementsfor proper typing.Extract complex conditions to $derived. Keep your templates clean and your logic efficient.
Be consistent within your codebase. Pick patterns that work for your team and stick with them.
Don’t forget accessibility. Dynamic classes are often tied to state—make sure that state is also communicated to assistive technologies via ARIA attributes.
Use CSS custom properties for theming. When building theme toggles, CSS variables can simplify your class logic significantly.
Test your class logic. Especially for components with many variants, verify that all combinations produce the expected styles.
Quick Reference
Here’s a handy reference for all the class syntax patterns:
<!-- Component class props (TypeScript) -->
<script lang="ts">
import type { ClassValue } from 'svelte/elements'
let { class: className }: { class?: ClassValue } = $props()
</script>
<!-- Static classes (always applied) -->
<div class="card rounded shadow">Always these classes</div>
<!-- Dynamic string from variable -->
<div class={currentTheme}>Class from variable</div>
<!-- Object syntax: keys are classes, values are conditions -->
<div class={{ active: isActive, disabled: isDisabled }}>Truthy values get applied</div>
<!-- Object with multi-class keys -->
<div class={{ 'loading pulse': isLoading }}>Multiple classes, one condition</div>
<!-- Array syntax: truthy values are combined -->
<div class={['base', isActive && 'active', size === 'lg' && 'large']}>
Falsy values are filtered out
</div>
<!-- Nested arrays and objects -->
<div class={[baseClasses, { highlight: isHighlighted }, variant && variantClass]}>
Everything gets flattened
</div>
<!-- Ternary for either/or classes -->
<div class={[isOpen ? 'expanded' : 'collapsed']}>One or the other</div>
<!-- Combining static and dynamic -->
<div class={['always-applied', sometimes && 'conditional']}>Mix freely</div>
<!-- Computed class names -->
<div class={{ [`theme-${theme}`]: true, [`size-${size}`]: true }}>Dynamic class names</div>
<div class={['component-base', className]}>Consumer classes merged</div> Conclusion
Dynamic class management is one of those patterns you’ll use constantly in Svelte development. The declarative approach—describing what should be true rather than imperatively manipulating the DOM—leads to cleaner, more maintainable code. With object and array syntax, you have the tools to handle everything from simple toggles to complex, multi-variant design systems.
Svelte’s class handling transforms what would be messy string concatenation and imperative DOM manipulation in vanilla JavaScript into elegant, reactive expressions that automatically update when state changes.
The evolution from class: directive syntax to the new array and object literal forms represents Svelte 5’s commitment to leveraging modern JavaScript features for better developer experience. By understanding the flattening behavior, mastering conditional patterns, and utilizing ClassValue types for component APIs, you can build design systems that are both flexible and type-safe.
Whether you’re implementing simple toggles or orchestrating complex variant systems with dozens of conditional classes, Svelte’s declarative class management keeps your code readable and your components maintainable.
Key Takeaways
- Object syntax enables declarative class toggling with
class={{ active: isActive, disabled: !enabled }}where truthy values include the key as a class name - Array syntax supports mixed static and conditional classes via
class={['base-class', condition && 'conditional-class']}with automatic filtering of falsy values - Arrays automatically flatten nested structures -
class={[baseClasses, [variantClasses, conditionalClasses]]}becomes a single space-separated class string - Short-circuit evaluation provides inline conditionals using
&&for “apply if true” and ternary operators for “either/or” class selection - Component class props use
ClassValuetype -let { class: className }: { class?: ClassValue } = $props()accepts strings, arrays, objects, or nested combinations - Class merging follows specificity rules - CSS specificity determines which styles apply when multiple classes define the same property, not the order in Svelte
$derivedenables complex computed classes for multi-condition logic:let classes = $derived({ active, disabled, [variantClass]: true })- Template literal syntax for dynamic class names -
class={{ [`theme-${theme}`]: true }}generates class names from runtime values
See Also
- Official Svelte 5 Documentation -
class: - CSS Specificity - Understanding how conflicting class styles are resolved
$derived- Reactive computed values for complex class logic$props()- Component props system for class prop handling- TypeScript with Svelte - Type-safe class value definitions
style:directive - Dynamic inline styles as an alternative to classes- CSS Modules - Scoped CSS as an alternative styling approach
- Tailwind CSS with Svelte - Utility-first CSS framework integration