---
title: "Class and style bindings, without the mess · vuejs.school"
description: "Object and array syntax for class bindings, a computed once conditions pile up, styles with units and CSS variables, and where a class lands on a component."
canonical_url: "https://vuejs.school/community/blog/class-and-style-bindings-without-the-mess"
meta:
  "og:description": "Object and array syntax for class bindings, a computed once conditions pile up, styles with units and CSS variables, and where a class lands on a component."
  "og:image": "https://vuejs.school/_og/d/a_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNz%2Cdescription_~T2JqZWN0IGFuZCBhcnJheSBzeW50YXggZm9yIGNsYXNzIGJpbmRpbmdzLCBhIGNvbXB1dGVkIG9uY2UgY29uZGl0aW9ucyBwaWxlIHVwLCBzdHlsZXMgd2l0aCB1bml0cyBhbmQgQ1NTIHZhcmlhYmxlcywgYW5kIHdoZXJlIGEgY2xhc3MgbGFuZHMgb24gYSBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jbGFzcy1hbmQtc3R5bGUtYmluZGluZ3Mtd2l0aG91dC10aGUtbWVzcyI%2Cs_aS5msBhmjwbgWuES.png"
  "og:image:alt": "Class and style bindings, without the mess · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Class and style bindings, without the mess · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNz%2Cdescription_~T2JqZWN0IGFuZCBhcnJheSBzeW50YXggZm9yIGNsYXNzIGJpbmRpbmdzLCBhIGNvbXB1dGVkIG9uY2UgY29uZGl0aW9ucyBwaWxlIHVwLCBzdHlsZXMgd2l0aCB1bml0cyBhbmQgQ1NTIHZhcmlhYmxlcywgYW5kIHdoZXJlIGEgY2xhc3MgbGFuZHMgb24gYSBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jbGFzcy1hbmQtc3R5bGUtYmluZGluZ3Mtd2l0aG91dC10aGUtbWVzcyI%2Cs_aS5msBhmjwbgWuES.png"
  "twitter:image:alt": "Class and style bindings, without the mess · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Toggle classes with the object syntax

The object form maps a class name to a condition. The class is present when the value is truthy and absent when it is not. It sits next to a plain `class` attribute, and Vue merges the two.

*components/StatusBadge.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const isActive = ref(true)
const hasError = ref(false)
</script>

<template>
  <span class="badge" :class="{ 'badge-active': isActive, 'badge-error': hasError }">
    Status
  </span>
</template>
```

This renders `<span class="badge badge-active">`. Flip `hasError` to true and `badge-error` is added; nothing else moves. Class names with a hyphen go in quotes because they are object keys. This is the shape for toggles, and most class bindings are toggles.

## Build class names from values with the array syntax

Sometimes the class name itself comes from data: a size, a colour, a variant. Then you want the array form, which takes a list of strings and adds them all. An object inside the array still toggles.

*components/BaseButton.vue*

```
<script setup lang="ts">
const props = defineProps<{
  size?: 'sm' | 'md' | 'lg'
  busy?: boolean
}>()
</script>

<template>
  <button type="button" :class="['btn', `btn-${props.size ?? 'md'}`, { 'btn-busy': props.busy }]">
    <slot />
  </button>
</template>
```

With `size="lg"` and `busy` set, the result is `class="btn btn-lg btn-busy"`; with no `size`, the fallback gives `btn-md`. Use the array when a name is built from a value, and keep the toggles as an object inside it. A ternary in the array, `isActive ? 'on' : ''`, works but reads worse than `{ on: isActive }`, so prefer the object.

## Move three or more conditions into a computed

Templates are for showing state, not for working it out. Once a class binding has a third condition, or a condition that depends on two values, the inline version becomes a chain of ternaries that nobody can check at a glance. Here is a task row's template written that way:

*components/TaskRow.vue**Breaks***

```
<template>
  <li :class="task.done ? 'row row-done' : task.dueSoon && !task.done ? 'row row-urgent' : task.assignee ? 'row row-assigned' : 'row'">
    {{ task.title }}
  </li>
</template>
```

Read it twice and you will spot the bug. A task that is both urgent and assigned gets `row-urgent` but not `row-assigned`, because a ternary chain stops at the first match.

The fix is a computed that returns an object. Each class gets its own condition, so one can no longer hide another, and Vue caches the result until a value it reads changes.

*components/TaskRow.vue**Works***

```
<script setup lang="ts">
import { computed } from 'vue'

interface Task {
  title: string
  done: boolean
  dueSoon: boolean
  assignee?: string
}

const props = defineProps<{ task: Task }>()

const rowClass = computed(() => ({
  'row-done': props.task.done,
  'row-urgent': props.task.dueSoon && !props.task.done,
  'row-assigned': Boolean(props.task.assignee),
}))
</script>

<template>
  <li class="row" :class="rowClass">{{ task.title }}</li>
</template>
```

The static `row` class moved to the plain attribute, so the computed only holds decisions. The urgent, assigned task now renders `class="row row-urgent row-assigned"`.

## Set inline styles with camelCase keys and units

`:style` takes an object too, with CSS properties as its keys. Write a property in camelCase, `backgroundColor` for `background-color`, or quote the kebab-case name.

Vue sets each value on the element's `style` property as it is and never adds a unit. A length needs its unit in the string: the browser ignores `width: 50` but applies `width: '50%'`. A property without a unit, such as `opacity`, takes a plain number. In this progress bar, the width arrives as a number and is set as a string with `%`:

*components/ProgressBar.vue*

```
<script setup lang="ts">
const props = defineProps<{ value: number; color?: string }>()
</script>

<template>
  <div class="track" role="progressbar" :aria-valuenow="value" aria-valuemin="0" aria-valuemax="100">
    <div class="fill" :style="{ width: `${props.value}%`, backgroundColor: props.color ?? 'currentColor' }" />
  </div>
</template>
```

Vue adds a vendor prefix at runtime when the browser needs one for a property. `:style` also accepts an array of style objects, merged in order, so a later object wins when two set the same property.

Keep `:style` for values the app only knows while it runs: a width in percent, a translate in pixels, a colour the user picked. Everything that can be a class should be a class. The styling then stays in one stylesheet, where it can use hover states and media queries, which an inline style cannot.

## Pass numbers to CSS through custom properties

A neat use of `:style` is to set a CSS custom property (a variable such as `--progress`) and let the stylesheet do the rest. The component passes one number; the CSS decides what it means.

*components/Ring.vue*

```
<script setup lang="ts">
const props = defineProps<{ progress: number }>()
</script>

<template>
  <div class="ring" :style="{ '--progress': props.progress }" />
</template>

<style scoped>
.ring {
  width: calc(var(--progress) * 1%);
  height: 4px;
  background: var(--brand, currentColor);
  transition: width 200ms;
}
</style>
```

`calc(var(--progress) * 1%)` turns the bare number into a percentage inside the CSS, so the script never builds strings. Vue passes a key that starts with `--` to the browser unchanged, so write it in quotes exactly as the CSS reads it.

Vue has a shortcut for the same idea: `v-bind()` inside a `<style>` block. Writing `width: v-bind('progress + "%"')` compiles to a custom property that Vue sets on the component's root element and keeps up to date. Its name is generated and hashed, though, so in practice only that component's own styles use it. Set the variable yourself, as above, when child components or other stylesheets need to read it.

## Where a class goes when you put it on a component

Put `class="ml-2"` on `<StatusBadge />` and the class ends up on the badge's root `<span>`, merged with the `badge` class already there. This is Vue's fallthrough behaviour: any attribute you pass that the component did not declare as a prop or an emit is added to its single root element. `:class` and `:style` bindings fall through the same way, and so do listeners such as `@click`.

## Place the class yourself when a component has two roots

A component with several root elements gets no automatic fallthrough, because Vue does not know which element should receive the class. The usual case is a `<dt>` and `<dd>` pair. A list of statistics is a `<dl>`, each item is a term and a value, and the component that renders one item has two root elements because a wrapping `<div>` would break the grid the parent lays them out on.

Pass that component a class and nothing is applied. In development Vue prints a warning instead: `Extraneous non-props attributes (class) were passed to component but could not be automatically inherited because component renders fragment or text or teleport root nodes.` Bind `$attrs.class` on the element that should carry the class:

*components/StatItem.vue*

```
<script setup lang="ts">
defineProps<{ label: string; value: string }>()
</script>

<template>
  <dt class="stat-label">{{ label }}</dt>
  <dd class="stat-value" :class="$attrs.class">{{ value }}</dd>
</template>
```

Now `<StatItem label="Open" value="12" class="is-high" />` renders `<dd class="stat-value is-high">`, the `<dt>` keeps its own class only, and the warning goes away. There is no `inheritAttrs: false` here. A component with two roots never inherits automatically, and reading `$attrs` in the template is what tells Vue you have placed the attributes yourself.

## Send attributes to an inner element instead of the root

Often the element that should receive the class is not the root. A text field wraps its `<input>` in a `<label>`, and a `placeholder` or an `aria-describedby` passed from outside belongs on the input. Turn automatic inheritance off with `defineOptions`, then bind `$attrs` where you want it.

*components/TextField.vue*

```
<script setup lang="ts">
defineOptions({ inheritAttrs: false })

defineProps<{ label: string }>()
const model = defineModel<string>()
</script>

<template>
  <label class="field">
    <span class="field-label">{{ label }}</span>
    <input v-model="model" v-bind="$attrs" />
  </label>
</template>
```

`v-bind="$attrs"` with no argument spreads every fallthrough attribute onto the input, listeners included. Now `<TextField label="Email" class="wide" placeholder="you@example.com" />` puts `wide` and the placeholder on the input, and the label keeps its own class.

The template always sees the latest `$attrs`. If the component's script needs one of these values, in a `computed` or a `watch`, declare it as a prop instead: the [Vue docs on fallthrough attributes](https://vuejs.org/guide/components/attrs.html#accessing-fallthrough-attributes-in-javascript) say the attributes object is not reactive and recommend a prop when you need reactivity.

**Check it stuck**3 questions from the Class and style bindings quiz.

[**Practice now**](https://vuejs.school/learn/beginner/class-and-style-bindings?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 5 min read**Forms and inputs: v-model modifiers, checkboxes and selects**](https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects) [Tutorial · 6 min read**Computed properties: cache, don't recalculate**](https://vuejs.school/community/blog/computed-properties-cache-dont-recalculate)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/class-and-style-bindings-without-the-mess#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-05-22","description":"Object and array syntax for class bindings, a computed once conditions pile up, styles with units and CSS variables, and where a class lands on a component.","name":"Class and style bindings, without the mess","url":"https://vuejs.school/community/blog/class-and-style-bindings-without-the-mess","breadcrumb":{"@id":"https://vuejs.school/community/blog/class-and-style-bindings-without-the-mess#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/class-and-style-bindings-without-the-mess"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/class-and-style-bindings-without-the-mess#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-05-22","description":"Object and array syntax for class bindings, a computed once conditions pile up, styles with units and CSS variables, and where a class lands on a component.","headline":"Class and style bindings, without the mess","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNz%2Cdescription_~T2JqZWN0IGFuZCBhcnJheSBzeW50YXggZm9yIGNsYXNzIGJpbmRpbmdzLCBhIGNvbXB1dGVkIG9uY2UgY29uZGl0aW9ucyBwaWxlIHVwLCBzdHlsZXMgd2l0aCB1bml0cyBhbmQgQ1NTIHZhcmlhYmxlcywgYW5kIHdoZXJlIGEgY2xhc3MgbGFuZHMgb24gYSBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jbGFzcy1hbmQtc3R5bGUtYmluZGluZ3Mtd2l0aG91dC10aGUtbWVzcyI%2Cs_aS5msBhmjwbgWuES.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/class-and-style-bindings-without-the-mess#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/class-and-style-bindings-without-the-mess#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/class-and-style-bindings-without-the-mess#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Class and style bindings, without the mess","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNz%2Cdescription_~T2JqZWN0IGFuZCBhcnJheSBzeW50YXggZm9yIGNsYXNzIGJpbmRpbmdzLCBhIGNvbXB1dGVkIG9uY2UgY29uZGl0aW9ucyBwaWxlIHVwLCBzdHlsZXMgd2l0aCB1bml0cyBhbmQgQ1NTIHZhcmlhYmxlcywgYW5kIHdoZXJlIGEgY2xhc3MgbGFuZHMgb24gYSBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jbGFzcy1hbmQtc3R5bGUtYmluZGluZ3Mtd2l0aG91dC10aGUtbWVzcyI%2Cs_aS5msBhmjwbgWuES.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNzIMK3IHZ1ZWpzLnNjaG9vbA%2Cc_OgCard.takumi%2Ctitle_~Q2xhc3MgYW5kIHN0eWxlIGJpbmRpbmdzLCB3aXRob3V0IHRoZSBtZXNz%2Cdescription_~T2JqZWN0IGFuZCBhcnJheSBzeW50YXggZm9yIGNsYXNzIGJpbmRpbmdzLCBhIGNvbXB1dGVkIG9uY2UgY29uZGl0aW9ucyBwaWxlIHVwLCBzdHlsZXMgd2l0aCB1bml0cyBhbmQgQ1NTIHZhcmlhYmxlcywgYW5kIHdoZXJlIGEgY2xhc3MgbGFuZHMgb24gYSBjb21wb25lbnQu%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9jbGFzcy1hbmQtc3R5bGUtYmluZGluZ3Mtd2l0aG91dC10aGUtbWVzcyI%2Cs_aS5msBhmjwbgWuES.png"}]}
```