Dynamic Inline Styles, the Svelte Way
There’s a moment every UI developer encounters: you need a style that depends on data. Maybe it’s a progress bar whose width matches a percentage, a color that comes from user preferences, or a position that tracks the mouse cursor. These aren’t static CSS classes—they’re values that change, and they need to flow directly into the element’s style.
In vanilla JavaScript, you’d write element.style.width = percentage + '%'. In JSX-based frameworks, you’d use a style object. Both approaches work but feel clunky—string concatenation is error-prone, and style objects add visual noise.
Svelte’s style: directive gives you a declarative, reactive way to set individual CSS properties directly on elements. It’s the inline-style equivalent of the class: directive: clean syntax, reactive updates, and zero overhead. You write style:width="{percentage}%" and Svelte keeps the DOM in sync automatically.
This article covers the style: directive comprehensively: basic syntax, shorthand forms, dynamic expressions, the !important modifier, how it interacts with the style attribute, and real-world patterns where it shines.
The Basics
Setting a Single Style Property
The simplest form of the style: directive sets one CSS property to a static value:
<div style:color="red">This text is red</div> This is equivalent to writing <div style="color: red;">, but the directive form becomes powerful when values are dynamic. Let’s see that next.
Dynamic Values with Expressions
The real power of style: shows up when you bind CSS properties to reactive state:
<script>
let progress = $state(0)
function startProgress() {
const interval = setInterval(() => {
progress += 1
if (progress >= 100) clearInterval(interval)
}, 50)
}
</script>
<button onclick={startProgress}>Start</button>
<div class="progress-track">
<div class="progress-bar" style:width="{progress}%">
{progress}%
</div>
</div>
<style>
.progress-track {
width: 100%;
height: 2rem;
background: #e5e7eb;
border-radius: 9999px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #3b82f6;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
font-weight: 600;
transition: width 0.1s ease;
}
</style> Every time progress changes, Svelte updates only the width property on that element. No diffing, no re-rendering the whole component—just a surgical element.style.width = ... call.
The Shorthand Form
When the CSS property name matches the variable name, you can use the shorthand form:
<script>
let color = $state('rebeccapurple')
</script>
<!-- Long form: explicit assignment -->
<p style:color={color}>Hello!</p>
<!-- Shorthand: variable name matches CSS property name -->
<p style:color>Hello!</p> The shorthand style:color is equivalent to style:color={color}. This is particularly nice for common properties:
<script>
let opacity = $state(1)
let transform = $state('none')
let color = $state('#1a1a1a')
</script>
<div style:opacity style:transform style:color>Styled element</div> Clean, readable, and reactive. Each property updates independently when its corresponding variable changes.
Multiple Style Properties
You can chain multiple style: directives on a single element:
<script>
let x = $state(0)
let y = $state(0)
let scale = $state(1)
function handleMouseMove(event) {
x = event.clientX
y = event.clientY
}
</script>
<svelte:window onmousemove={handleMouseMove} />
<div class="follower" style:left="{x}px" style:top="{y}px" style:transform="scale({scale})">👋</div>
<style>
.follower {
position: fixed;
pointer-events: none;
font-size: 2rem;
transition: transform 0.15s ease;
}
</style> Each style: directive manages its own property independently. When x changes, only left updates. When scale changes, only transform updates. Svelte doesn’t rewrite the entire style attribute—it touches only what changed.
Conditional and Computed Styles
Ternary Expressions
Use JavaScript expressions for conditional styling:
<script>
let isError = $state(false)
let isDisabled = $state(false)
</script>
<input
type="text"
style:border-color={isError ? '#ef4444' : '#d1d5db'}
style:opacity={isDisabled ? 0.5 : 1}
style:cursor={isDisabled ? 'not-allowed' : 'text'}
disabled={isDisabled}
/> Computed Values from Derived State
Combine style: with $derived for styles that depend on calculations:
<script>
let items = $state([
{ name: 'Design', hours: 12 },
{ name: 'Development', hours: 28 },
{ name: 'Testing', hours: 8 },
{ name: 'Deployment', hours: 2 }
])
let totalHours = $derived(items.reduce((sum, item) => sum + item.hours, 0))
</script>
<div class="bar-chart">
{#each items as item}
{@const percentage = (item.hours / totalHours) * 100}
<div class="bar-row">
<span class="label">{item.name}</span>
<div class="bar" style:width="{percentage}%">
{item.hours}h
</div>
</div>
{/each}
</div>
<style>
.bar-chart {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.bar-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.label {
width: 7rem;
text-align: right;
font-size: 0.875rem;
color: #6b7280;
}
.bar {
height: 2rem;
background: #3b82f6;
border-radius: 4px;
display: flex;
align-items: center;
padding-left: 0.5rem;
color: white;
font-size: 0.75rem;
font-weight: 600;
min-width: 3rem;
transition: width 0.3s ease;
}
</style> Nullish Values Are Removed
When a style: directive receives null or undefined, Svelte removes that property entirely from the element:
<script>
let highlight = $state(false)
</script>
<button onclick={() => (highlight = !highlight)}> Toggle highlight </button>
<!-- When highlight is false, background is completely removed (not set to "undefined") -->
<p style:background={highlight ? 'yellow' : null}>This paragraph might be highlighted</p> This is a clean way to toggle a style on and off without leaving empty or invalid values in the DOM. When highlight is false, the paragraph won’t have a background property at all—Svelte calls element.style.removeProperty('background').
The !important Modifier
Sometimes you need an inline style to override higher-specificity rules—especially when battling third-party CSS. The style: directive supports the |important modifier:
<script>
let userColor = $state('#3b82f6')
</script>
<!-- The |important modifier adds !important to the inline style -->
<div style:color|important={userColor}>
This color will override even high-specificity CSS rules
</div> This generates: style="color: #3b82f6 !important;".
WarningUse
|importantsparingly. It’s a legitimate tool when integrating with CSS you don’t control (third-party widgets, CMS-injected styles), but overusing it in your own styles creates specificity wars that are painful to debug. If you need|importantfor your own styles, it’s usually a sign that your CSS architecture needs restructuring.
Real-World Use Case: User-Customizable Themes
A practical scenario where |important is justified:
<script>
let { userTheme } = $props()
// User-defined overrides should win over component library defaults
let brandColor = $derived(userTheme?.brandColor ?? null)
let fontFamily = $derived(userTheme?.fontFamily ?? null)
</script>
<div class="widget">
<h2 style:color|important={brandColor} style:font-family|important={fontFamily}>
{userTheme?.title ?? 'Widget Title'}
</h2>
<div class="widget-body">
<p>Widget content that should respect user's brand colors.</p>
</div>
</div>
<style>
.widget {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
h2 {
padding: 1rem;
margin: 0;
background: #f9fafb;
font-size: 1.125rem;
/* This will be overridden by the |important directive when brandColor is set */
color: #1f2937;
}
.widget-body {
padding: 1rem;
}
</style> When brandColor is null, the |important style is removed entirely and the scoped CSS takes effect. When the user sets a brand color, it wins—even over the scoped color: #1f2937.
Interaction with the style Attribute
You can use both the style attribute and style: directives on the same element. The style: directive takes precedence for any property it manages:
<script>
let fontSize = $state('1rem')
</script>
<!-- The style attribute sets baseline styles -->
<!-- The style: directive overrides font-size specifically -->
<div style="color: navy; padding: 1rem; font-size: 0.875rem;" style:font-size={fontSize}>
The font-size comes from the directive, everything else from the attribute
</div> How this works under the hood:
- Svelte first applies the
styleattribute string as-is - Then it sets each
style:property individually viaelement.style.setProperty() - Since individual property assignments happen after the attribute, they win
This means you can use the style attribute for static baseline styles and style: directives for the dynamic parts. However, for clarity, prefer one approach per element when possible.
Dynamic style Attribute vs style: Directive
You might wonder: why not just use a dynamic style attribute?
<script>
let width = $state(50)
let color = $state('blue')
</script>
<!-- Approach 1: Dynamic style attribute (string concatenation) -->
<div style="width: {width}%; color: {color};">Content</div>
<!-- Approach 2: style: directives -->
<div style:width="{width}%" style:color>Content</div> Both work, but style: directives have key advantages. When width changes, the style attribute approach rewrites the entire style string. The style: directive only touches width, leaving color untouched. There’s also cleaner syntax (no string interpolation, no semicolons to manage), null handling that can cleanly remove individual properties, and the |important modifier—all unavailable on a plain style attribute.
One more behaviour is worth knowing: style: directives take precedence over the style attribute even when the attribute uses !important. This is counterintuitive but it’s how Svelte works:
<!-- Directive wins — this renders red, not blue -->
<div style:color="red" style="color: blue">Red</div>
<!-- Directive wins even here — still red despite !important on the attribute -->
<div style:color="red" style="color: blue !important">Still red</div> The directive always wins. Keep this in mind if you ever combine both on the same element.
Real-World Patterns
Pattern 1: Draggable Elements
A classic use of dynamic inline styles—positioning elements that the user moves around:
<script>
let isDragging = $state(false)
let x = $state(100)
let y = $state(100)
let offsetX = 0
let offsetY = 0
function handleMouseDown(event) {
isDragging = true
offsetX = event.clientX - x
offsetY = event.clientY - y
}
function handleMouseMove(event) {
if (!isDragging) return
x = event.clientX - offsetX
y = event.clientY - offsetY
}
function handleMouseUp() {
isDragging = false
}
</script>
<svelte:window onmousemove={handleMouseMove} onmouseup={handleMouseUp} />
<div
class="draggable"
class:dragging={isDragging}
style:left="{x}px"
style:top="{y}px"
style:cursor={isDragging ? 'grabbing' : 'grab'}
onmousedown={handleMouseDown}
role="button"
tabindex="0"
>
Drag me!
</div>
<style>
.draggable {
position: absolute;
padding: 1rem 1.5rem;
background: white;
border: 2px solid #3b82f6;
border-radius: 8px;
font-weight: 500;
user-select: none;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.15s ease;
}
.dragging {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 10;
}
</style> Notice how style: handles the dynamic positioning (left, top, cursor) while scoped CSS handles the static appearance. Each tool does what it’s best at.
Pattern 2: Color Picker Preview
When styles come directly from user input:
<script>
let hue = $state(210)
let saturation = $state(80)
let lightness = $state(50)
let alpha = $state(1)
let cssColor = $derived(`hsla(${hue}, ${saturation}%, ${lightness}%, ${alpha})`)
</script>
<div class="picker">
<div class="controls">
<label>
Hue: {hue}°
<input type="range" min="0" max="360" bind:value={hue} />
</label>
<label>
Saturation: {saturation}%
<input type="range" min="0" max="100" bind:value={saturation} />
</label>
<label>
Lightness: {lightness}%
<input type="range" min="0" max="100" bind:value={lightness} />
</label>
<label>
Alpha: {alpha}
<input type="range" min="0" max="1" step="0.01" bind:value={alpha} />
</label>
</div>
<div class="preview-area">
<div class="swatch" style:background-color={cssColor}></div>
<code>{cssColor}</code>
</div>
</div>
<style>
.picker {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
padding: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 12px;
}
.controls {
display: flex;
flex-direction: column;
gap: 1rem;
}
label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
color: #374151;
}
input[type='range'] {
width: 100%;
}
.preview-area {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.swatch {
width: 100%;
aspect-ratio: 1;
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.1);
transition: background-color 0.1s ease;
}
code {
font-size: 0.875rem;
background: #f4f4f5;
padding: 0.25rem 0.75rem;
border-radius: 4px;
}
</style> Pattern 3: Responsive Grid with User Controls
Combining multiple dynamic styles for a configurable layout:
<script>
let columns = $state(3)
let gap = $state(1)
let padding = $state(1.5)
let items = $state(Array.from({ length: 9 }, (_, i) => `Item ${i + 1}`))
</script>
<div class="toolbar">
<label>
Columns: {columns}
<input type="range" min="1" max="6" bind:value={columns} />
</label>
<label>
Gap: {gap}rem
<input type="range" min="0" max="3" step="0.25" bind:value={gap} />
</label>
<label>
Padding: {padding}rem
<input type="range" min="0" max="3" step="0.25" bind:value={padding} />
</label>
</div>
<div
class="grid"
style:grid-template-columns="repeat({columns}, 1fr)"
style:gap="{gap}rem"
style:padding="{padding}rem"
>
{#each items as item}
<div class="cell">{item}</div>
{/each}
</div>
<style>
.toolbar {
display: flex;
gap: 2rem;
padding: 1rem;
margin-bottom: 1rem;
background: #f9fafb;
border-radius: 8px;
}
label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
}
.grid {
display: grid;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.cell {
padding: 1.5rem;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 6px;
text-align: center;
font-weight: 500;
color: #1e40af;
}
</style> Pattern 4: Animation with Reactive Transforms
A card that responds to mouse position with a 3D tilt effect:
<script>
let rotateX = $state(0)
let rotateY = $state(0)
let isHovering = $state(false)
function handleMouseMove(event) {
const rect = event.currentTarget.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
// Convert mouse position to rotation degrees (-15 to 15)
rotateY = ((event.clientX - centerX) / (rect.width / 2)) * 15
rotateX = -((event.clientY - centerY) / (rect.height / 2)) * 15
}
function handleMouseLeave() {
rotateX = 0
rotateY = 0
isHovering = false
}
</script>
<div
class="tilt-card"
class:hovering={isHovering}
style:transform="perspective(600px) rotateX({rotateX}deg) rotateY({rotateY}deg)"
onmouseenter={() => (isHovering = true)}
onmousemove={handleMouseMove}
onmouseleave={handleMouseLeave}
role="presentation"
>
<h3>Interactive Card</h3>
<p>Move your mouse over this card to see it tilt in 3D space.</p>
</div>
<style>
.tilt-card {
width: 300px;
padding: 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
transition:
transform 0.1s ease,
box-shadow 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.hovering {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
}
h3 {
margin: 0 0 0.75rem;
font-size: 1.25rem;
}
p {
margin: 0;
font-size: 0.875rem;
opacity: 0.9;
line-height: 1.5;
}
</style> When to Use style: vs Other Approaches
Choosing the right styling approach matters. Here’s a decision framework:
Is the style value known at build time?
├── Yes → Use scoped CSS in <style> block
│ (better performance, no runtime cost)
│
└── No → Is the value one of a few known options?
│
├── Yes → Use conditional classes
│ class:active={isActive}
│ class={{ dark: isDark, compact: isCompact }}
│
└── No → Is it a continuous/computed value?
│
├── Yes → Use style: directive ✨
│ style:width="{percent}%"
│ style:color={userColor}
│ style:transform="rotate({deg}deg)"
│
└── Should it be customizable by parent?
│
└── Yes → Use CSS custom properties (--style-props)
<Component --accent="red" /> The key insight here is that classes are for discrete states (active/inactive, dark/light, small/medium/large) while inline styles are for continuous values (positions, sizes, colors from data, animations). The style: directive makes the inline style case ergonomic and reactive without requiring manual DOM manipulation.
TipIf you find yourself creating dozens of classes like
.opacity-10,.opacity-20, ….opacity-100, that’s a strong signal to usestyle:opacityinstead. Conversely, if you’re usingstyle:displayto toggle betweennoneandblock, a conditional class is usually cleaner.
Common Pitfalls
Pitfall 1: Forgetting Units
CSS properties that need units will silently fail without them:
<script>
let size = $state(200)
</script>
<!-- Avoid: Missing unit — browser ignores this -->
<div style:width={size}>Content</div>
<!-- Preferred: Include the unit in the expression -->
<div style:width="{size}px">Content</div> Pitfall 2: Camel Case vs Kebab Case
The style: directive uses the CSS property name (kebab-case), not the JavaScript property name:
<!-- Avoid: JavaScript property name -->
<div style:backgroundColor="red">
<!-- Preferred: CSS property name -->
<div style:background-color="red"> Pitfall 3: Overusing Inline Styles
<!-- Avoid: Too much — these are all static values -->
<div
style:display="flex"
style:gap="1rem"
style:padding="2rem"
style:border-radius="8px"
style:background="white"
>
<!-- Preferred: Use scoped CSS for static styles, style: for dynamic ones -->
<div class="card" style:background={userBackground}> If a value never changes, it belongs in the <style> block. Reserve style: for values that are genuinely dynamic.
Pitfall 4: Conflicting with style Attribute
<script>
let color = $state('red')
</script>
<!-- The style: directive wins for 'color', but the attribute sets 'padding' -->
<div style="color: blue; padding: 1rem;" style:color>This is red, not blue (directive wins)</div> The style: directive always takes precedence over the same property in the style attribute. While this is well-defined behavior, it can be confusing. Prefer using one approach consistently per element.
Quick Reference
Static Value
<div style:color="red">...</div> Dynamic Expression
<div style:color={myColor}>...</div>
<div style:width="{percentage}%">...</div>
<div style:transform="rotate({angle}deg)">...</div> Shorthand (Variable Name Matches Property)
<script>
let color = $state('rebeccapurple')
let opacity = $state(0.8)
</script>
<div style:color style:opacity>...</div> Multiple Styles
<div style:color style:width="12rem" style:background-color={darkMode ? 'black' : 'white'}>...</div> Important Modifier
<div style:color|important={overrideColor}>...</div> Null Removes the Property
<div style:background={isActive ? 'yellow' : null}>...</div> What’s Next?
You’ve mastered dynamic inline styles. The final piece of Svelte’s styling system is the most powerful for component architecture:
- CSS Custom Properties — The cleanest mechanism for parent-controlled component theming