Vue.js 15 🟢 Dynamic Class and Style Attribute Bindings
A class is a string. A style is a string. Both can be bound with :class and :style using the same expression syntax as any other attribute. But Vue provides additional syntax for these two attributes specifically, because they are the ones that change most often in response to state. A button’s class depends on whether it is active. An element’s style depends on a computed position. Writing the string manually—concatenating pieces, checking conditions, filtering out empty entries—is repetitive and error-prone. Vue’s object and array syntax for :class and :style turns these computations into data structures that the framework serializes.
This chapter covers dynamic class and style bindings in full. You will learn the object syntax for conditional classes, the array syntax for applying multiple classes, and how the two can be mixed. You will see the same patterns applied to :style, with the additional consideration that style property names can be camelCase or kebab-case. You will also see how static and dynamic bindings combine, what happens when they conflict, and the pitfalls that produce classes that do not apply or styles that do not render.
Key point: :class and :style accept strings, objects, and arrays. The object syntax toggles keys based on the truthiness of their values. The array syntax applies each entry in order. When a static class or style attribute is also present, Vue merges the two rather than replacing one with the other.
Why dynamic class and style bindings exist
The conditional class problem. Most conditional classes are toggles: active when a tab is selected, error when a field is invalid, disabled when a button is not available. Writing :class="isActive ? 'active' : ''" works for one class, but it does not scale. Two conditions require nested ternaries. Three require something unreadable. The object syntax expresses each condition as a key with a boolean value, and the framework serializes the truthy keys into a class string.
The multiple class problem. An element often needs several classes, some static and some dynamic. class="btn" provides the base, and :class adds the dynamic ones. Vue merges the two. The static class is always present; the dynamic classes are added or removed based on state. This is why class and :class can coexist on the same element without conflict.
The inline style problem. Inline styles are discouraged for layout that belongs in a stylesheet, but they are appropriate for values that are computed at runtime: a progress bar’s width, a tooltip’s position, a color selected by the user. The :style binding accepts an object whose keys are CSS property names and whose values are the computed values. This is the standard way to apply runtime-computed styles in Vue.
The merging problem. When a static class and a dynamic :class are both present, Vue concatenates them. When a static style and a dynamic :style are both present, Vue merges them, with the dynamic binding taking precedence for conflicting properties. Understanding this merge behavior prevents the confusion of “why is my class not applying” or “why is my style being overridden.”
The readability problem. Object and array syntax keeps the template readable. A class binding that would be a long ternary is instead a short object. A style binding that would be string concatenation is instead a plain object with named properties. The template describes the relationship between state and appearance, and the framework handles the serialization.
a. Class binding: object and array syntax
The object syntax toggles classes based on the truthiness of their values. Each key is a class name; each value is a condition.
<template>
<div :class="{ active: isActive, 'text-danger': hasError }">
Content
</div>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
const hasError = ref(false)
</script>
When isActive is true and hasError is false, the rendered class is active. When both are true, it is active text-danger. When both are false, no class is added.
The object can be inline, as above, or a computed property. A computed property is preferred when the logic is more than a simple toggle, because it can be named and tested.
<div :class="statusClasses">Content</div>
const statusClasses = computed(() => ({
active: isActive.value && !hasError.value,
'text-danger': hasError.value,
disabled: !isActive.value && !hasError.value
}))
The array syntax applies a list of classes. Each entry can be a string, an object, or a nested array.
<div :class="['base-class', isActive ? 'active' : '', 'static-class']">
Empty strings in the array are ignored. This is why the ternary pattern works: when the condition is false, the entry is '', which produces no class.
Arrays and objects combine. The common pattern is a base class, a conditional object, and additional strings.
<div :class="['btn', { 'btn-active': isActive, 'btn-disabled': isDisabled }]">
A static class attribute can be present alongside :class. Vue merges them:
<div class="card" :class="{ 'card-elevated': isElevated }">
The rendered element has class="card card-elevated" when isElevated is true, and class="card" when it is false. The order is not guaranteed, but for CSS it does not matter—specificity is determined by the selectors, not the order of class names in the attribute.
b. Style binding: object and array syntax
The style binding accepts an object whose keys are CSS property names and whose values are the computed values.
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">
Styled text
</div>
Property names can be camelCase (fontSize) or kebab-case ('font-size'). CamelCase is more common in JavaScript. Kebab-case must be quoted when used as an object key.
<div :style="{ 'font-size': '16px', 'background-color': bg }">
The array syntax allows multiple style objects, which are merged in order. Later objects override earlier ones for the same property.
<div :style="[baseStyles, overrideStyles]">
This is useful for composing styles from different sources: a base style and a conditional override.
const baseStyles = { padding: '10px', borderRadius: '4px' }
const overrideStyles = computed(() => ({
backgroundColor: isActive.value ? 'blue' : 'gray'
}))
A static style attribute can coexist with :style. Vue merges them, with the dynamic binding taking precedence for conflicting properties.
<div style="color: red" :style="{ color: textColor }">
If textColor is 'blue', the rendered style is color: blue. The static value is overridden. For non-conflicting properties, both are applied.
Vue automatically prefixes CSS properties that need vendor prefixes. :style="{ userSelect: 'none' }" produces the appropriate -webkit-user-select and -moz-user-select prefixes in addition to the standard property. This is one of the conveniences of using :style rather than setting the style string manually.
Multiple values for a single property, such as a fallback, can be expressed as an array:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }">
The browser applies the last value it supports. This is the standard CSS fallback pattern, expressed as an array in the binding.
c. Merging, computed properties, and conflicts
The merge behavior between static and dynamic bindings is consistent, but it has rules worth knowing.
Class merging. A static class and a dynamic :class are concatenated. Both contribute to the element’s class list. There is no conflict—class names are additive.
<div class="card" :class="{ active: isActive }">
<!-- isActive true: class="card active" -->
<!-- isActive false: class="card" -->
Style merging. A static style and a dynamic :style are merged. For properties present in both, the dynamic value wins.
<div style="color: red; padding: 10px" :style="{ color: 'blue' }">
<!-- Rendered: color: blue; padding: 10px -->
Component class inheritance. When :class is applied to a component with a single root element, the class is added to that root element. This is the default behavior for attributes that are not declared as props.
<MyButton :class="{ primary: isPrimary }" />
<!-- The class is added to MyButton's root element -->
If the component has multiple root elements, the class must be explicitly forwarded with $attrs, or the component must declare which root receives it.
Computed properties for complex logic. When the class or style computation involves more than a simple toggle or a single condition, a computed property keeps the template clean.
const buttonClasses = computed(() => {
const classes = ['btn']
if (props.primary) classes.push('btn-primary')
if (props.disabled) classes.push('btn-disabled')
if (props.loading) classes.push('btn-loading')
return classes
})
The computed returns an array or object, and the template binds it with :class="buttonClasses". The logic lives in JavaScript, where it can be tested and named.
The specificity trap. A dynamic class that applies a style may be overridden by a more specific selector in the stylesheet. Vue applies the class, but CSS determines which rule wins. When a dynamic class does not seem to take effect, the issue is often specificity, not the binding.
Complete Example Session
<!-- ============================================ -->
<!-- PART 1: BASIC OBJECT SYNTAX -->
<!-- ============================================ -->
<template>
<div :class="{ active: isActive, 'text-danger': hasError }">
Content
</div>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
const hasError = ref(false)
</script>
<!-- ============================================ -->
<!-- PART 2: ARRAY SYNTAX -->
<!-- ============================================ -->
<template>
<div :class="['base', isActive ? 'active' : '', 'always']">
Content
</div>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
</script>
<!-- ============================================ -->
<!-- PART 3: ARRAY WITH OBJECT -->
<!-- ============================================ -->
<template>
<div :class="['btn', { 'btn-active': isActive, 'btn-disabled': isDisabled }]">
Button
</div>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
const isDisabled = ref(false)
</script>
<!-- ============================================ -->
<!-- PART 4: STATIC AND DYNAMIC CLASS -->
<!-- ============================================ -->
<template>
<div class="card" :class="{ elevated: isElevated }">
Card content
</div>
</template>
<script setup>
import { ref } from 'vue'
const isElevated = ref(true)
</script>
<!-- ============================================ -->
<!-- PART 5: COMPUTED CLASS OBJECT -->
<!-- ============================================ -->
<template>
<div :class="statusClasses">Status</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const isActive = ref(true)
const hasError = ref(false)
const statusClasses = computed(() => ({
active: isActive.value && !hasError.value,
'text-danger': hasError.value,
disabled: !isActive.value && !hasError.value
}))
</script>
<!-- ============================================ -->
<!-- PART 6: BASIC STYLE BINDING -->
<!-- ============================================ -->
<template>
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">
Styled text
</div>
</template>
<script setup>
import { ref } from 'vue'
const textColor = ref('#333')
const fontSize = ref(16)
</script>
<!-- ============================================ -->
<!-- PART 7: KEBAB-CASE STYLE PROPERTIES -->
<!-- ============================================ -->
<template>
<div :style="{ 'font-size': '16px', 'background-color': bgColor }">
Styled text
</div>
</template>
<script setup>
import { ref } from 'vue'
const bgColor = ref('#f0f0f0')
</script>
<!-- ============================================ -->
<!-- PART 8: STYLE ARRAY -->
<!-- ============================================ -->
<template>
<div :style="[baseStyles, overrideStyles]">
Composed styles
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const isActive = ref(true)
const baseStyles = { padding: '10px', borderRadius: '4px' }
const overrideStyles = computed(() => ({
backgroundColor: isActive.value ? 'blue' : 'gray'
}))
</script>
<!-- ============================================ -->
<!-- PART 9: STATIC AND DYNAMIC STYLE -->
<!-- ============================================ -->
<template>
<div style="color: red; padding: 10px" :style="{ color: textColor }">
Content
</div>
</template>
<script setup>
import { ref } from 'vue'
const textColor = ref('blue')
// Rendered: color: blue; padding: 10px
</script>
<!-- ============================================ -->
<!-- PART 10: DYNAMIC PROGRESS BAR -->
<!-- ============================================ -->
<template>
<div class="progress">
<div
class="progress-bar"
:class="{ 'progress-bar--complete': progress === 100 }"
:style="{ width: progress + '%' }"
>
{{ progress }}%
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const progress = ref(75)
</script>
The ten parts covered object syntax, array syntax, array with object, static and dynamic class, computed class object, basic style binding, kebab-case properties, style array, static and dynamic style, and a dynamic progress bar.
Quick Reference
Class Binding Forms
| Form | Example | Use Case |
|---|---|---|
| String | :class="className" | Single dynamic class |
| Object | :class="{ active: isActive }" | Conditional toggles |
| Array | :class="[a, b, c]" | Multiple classes |
| Array + Object | :class="[a, { b: cond }]" | Mixed |
| Computed | :class="computedObj" | Complex logic |
Style Binding Forms
| Form | Example | Use Case |
|---|---|---|
| Object | :style="{ color: c }" | Named properties |
| Kebab-case | :style="{ 'font-size': s }" | Hyphenated properties |
| Array | :style="[base, override]" | Composed styles |
| Multi-value | :style="{ display: [a, b, c] }" | Fallback values |
| Computed | :style="computedStyle" | Complex logic |
Merging Behavior
| Scenario | Result |
|---|---|
class + :class | Concatenated |
style + :style | Merged, dynamic wins |
Component :class | Applied to root element |
| Multiple roots | Must forward manually |
Common Patterns
| Pattern | Purpose |
|---|---|
{ active: isActive } | Toggle a class |
[base, { cond: c }] | Base plus conditional |
computed(() => ({...})) | Named, testable logic |
width: progress + '%' | Computed dimension |
display: [a, b, c] | CSS fallback |
Best Practices
✅ Do This:
<!-- Use object syntax for conditional classes -->
<div :class="{ active: isActive }"> <!-- ✅ -->
<!-- Use array syntax for multiple classes -->
<div :class="['btn', 'btn-primary']"> <!-- ✅ -->
<!-- Combine static and dynamic -->
<div class="card" :class="{ elevated: isElevated }"> <!-- ✅ -->
<!-- Use computed for complex class logic -->
<div :class="statusClasses"> <!-- ✅ -->
<!-- Use object syntax for styles -->
<div :style="{ width: progress + '%' }"> <!-- ✅ -->
<!-- Use array for style fallbacks -->
<div :style="{ display: ['-webkit-box', 'flex'] }"> <!-- ✅ -->
<!-- Use computed for complex style logic -->
<div :style="computedStyle"> <!-- ✅ -->
❌ Don’t Do This:
<!-- Don't concatenate class strings manually -->
<div :class="'btn ' + (active ? 'active' : '')"> <!-- ❌ -->
<div :class="['btn', { active }]"> <!-- ✅ -->
<!-- Don't use nested ternaries for multiple conditions -->
<div :class="a ? 'x' : b ? 'y' : 'z'"> <!-- ❌ -->
<div :class="{ x: a, y: b && !a, z: !a && !b }"> <!-- ✅ -->
<!-- Don't forget to quote kebab-case properties -->
<div :style="{ font-size: s }"> <!-- ❌ -->
<div :style="{ 'font-size': s }"> <!-- ✅ -->
<!-- Don't expect dynamic style to override !important -->
<div style="color: red !important" :style="{ color: 'blue' }"> <!-- ❌ -->
<!-- Don't put all styling in :style if it belongs in CSS -->
<div :style="{ padding: '10px', margin: '5px' }"> <!-- ⚠️ -->
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Class not applying | CSS specificity | Check selector specificity |
| Style not overriding | !important in static style | Remove or restructure |
| Kebab-case ignored | Property not quoted | Quote the key |
| Class not on component root | Multiple root elements | Forward $attrs |
| Dynamic class not updating | Non-reactive condition | Use ref or computed |
| Array with empty strings | Falsy entries | They are ignored, safe |
| Style object mutated | Shared object reference | Use computed for dynamic values |
Real-World Examples
1. Active Tab
<button :class="{ 'tab--active': activeTab === tab.id }">
2. Form Field Error
<input :class="{ 'input--error': errors.email }">
3. Button Variants
<button :class="['btn', `btn--${variant}`, { 'btn--loading': loading }]">
4. Progress Bar
<div :style="{ width: progress + '%' }"></div>
5. Theme Color
<div :style="{ backgroundColor: themeColor }">
6. Conditional Elevation
<div class="card" :class="{ elevated: isHovered }">
7. Status Badge
<span :class="['badge', `badge--${status}`]">{{ status }}</span>
8. Dynamic Font Size
<h1 :style="{ fontSize: size + 'rem' }">Title</h1>
9. Grid Columns
<div :style="{ gridTemplateColumns: `repeat(${cols}, 1fr)` }">
10. Disabled State
<button :class="{ disabled: isDisabled }" :disabled="isDisabled">
Visual
Class Binding Decision Tree
┌─────────────────────────────────────────────────────────────┐
│ WHICH SYNTAX TO USE? │
│ │
│ Single dynamic class name? │
│ └── YES ──▶ :class="className" │
│ │
│ Toggle classes conditionally? │
│ └── YES ──▶ :class="{ active: isActive }" │
│ │
│ Apply a list of classes? │
│ └── YES ──▶ :class="[a, b, c]" │
│ │
│ Mix static, dynamic, and conditional? │
│ └── YES ──▶ :class="[base, { active: cond }]" │
│ │
│ Complex logic from multiple sources? │
│ └── YES ──▶ computed property │
│ │
└─────────────────────────────────────────────────────────────┘
Static and Dynamic Merging
┌─────────────────────────────────────────────────────────────┐
│ CLASS MERGING │
│ │
│ <div class="card" :class="{ active: isActive }"> │
│ │
│ isActive = true: │
│ class="card active" │
│ │
│ isActive = false: │
│ class="card" │
│ │
│ Class names are additive. No conflict. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ STYLE MERGING │
│ │
│ <div style="color: red" :style="{ color: 'blue' }"> │
│ │
│ Rendered: │
│ style="color: blue" │
│ │
│ Dynamic wins for conflicting properties. │
│ Non-conflicting properties are both applied. │
│ │
└─────────────────────────────────────────────────────────────┘
Object Syntax Serialization
┌─────────────────────────────────────────────────────────────┐
│ :class="{ active: true, error: false, disabled: true }" │
│ │
│ Vue iterates the object: │
│ │
│ active → true → include │
│ error → false → skip │
│ disabled → true → include │
│ │
│ Rendered: class="active disabled" │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ :class="['btn', '', 'primary']" │
│ │
│ Vue iterates the array: │
│ │
│ 'btn' → include │
│ '' → skip (empty string) │
│ 'primary' → include │
│ │
│ Rendered: class="btn primary" │
│ │
└─────────────────────────────────────────────────────────────┘
Component Class Inheritance
┌─────────────────────────────────────────────────────────────┐
│ SINGLE ROOT COMPONENT │
│ │
│ Parent: │
│ <MyButton :class="{ primary: isPrimary }" /> │
│ │
│ MyButton.vue: │
│ <template> │
│ <button>Click</button> │
│ </template> │
│ │
│ Rendered: │
│ <button class="primary">Click</button> │
│ │
│ Class falls through to root automatically. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ MULTIPLE ROOT COMPONENT │
│ │
│ MyButton.vue: │
│ <template> │
│ <button>Click</button> │
│ <span>Icon</span> │
│ </template> │
│ │
│ Class does not fall through automatically. │
│ Must forward with $attrs or declare inheritAttrs: false. │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Class object | :class="{ active: cond }" |
| Class array | :class="[a, b, c]" |
| Class mixed | :class="[a, { b: cond }]" |
| Style object | :style="{ color: c }" |
| Style kebab-case | :style="{ 'font-size': s }" |
| Style array | :style="[base, override]" |
| Style fallback | :style="{ display: [a, b] }" |
| Static + dynamic class | Concatenated |
| Static + dynamic style | Merged, dynamic wins |
| Component class | Falls through to root |
| Complex logic | Computed property |
Key takeaways:
- The object syntax toggles classes by truthiness. Each key is a class name, each value is a condition. Truthy conditions include the class; falsy conditions skip it. This replaces ternary chains for conditional classes.
- The array syntax applies multiple classes. Entries can be strings, objects, or nested arrays. Empty strings and falsy values are ignored, which makes the ternary pattern safe:
isActive ? 'active' : ''. - The two syntaxes combine. The common pattern is a base class string followed by an object of conditional classes:
:class="['btn', { 'btn-active': isActive }]". - Style bindings accept camelCase or kebab-case property names. Kebab-case keys must be quoted. Vue automatically prefixes vendor-prefixed properties, so
userSelectproduces both the standard and prefixed forms. - Static and dynamic bindings merge. A static
classis concatenated with the dynamic:class. A staticstyleis merged with the dynamic:style, with the dynamic value winning for conflicting properties. - Component class bindings fall through to the root element. When
:classis applied to a component with a single root, the class is added to that root. For multi-root components, the class must be forwarded explicitly. - Computed properties keep complex logic out of the template. When the class or style computation involves more than a simple toggle, a computed property returns the object or array, and the template binds it in one attribute.
- CSS specificity can override dynamic classes. A class that is correctly applied may still not produce the expected visual if a more specific rule in the stylesheet takes precedence. When a class appears not to work, check the CSS before checking the binding.
Remember: Dynamic class and style bindings are the primary mechanism for connecting component state to appearance. The object syntax expresses conditional toggles. The array syntax expresses composition. The two combine to cover the common cases. Static and dynamic bindings merge, so a base class and a dynamic modifier can coexist on the same element. Use computed properties when the logic is complex, and check CSS specificity when a class does not appear to apply. The goal is to describe the relationship between state and appearance declaratively, and to keep the template readable as the number of conditions grows.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!