skip to content

In Vue 3, how do :class object and array syntax work, and how would you build a button's classes from variant, size and disabled?

level: juniorimportance: must knowfreq 74%

answer

  1. strings, objects and arrays
  2. truthiness toggles an object key
  3. arrays may nest objects
  4. static class merges with :class

basics

~20 s

Vue's :class accepts a string, an object whose keys are added when their values are truthy, or an array of either. It merges with a static class attribute, so a computed can combine variant and size names with conditional flags like is-disabled.

solid answer

~40 s

`:class` is `v-bind` with extra value types. An **object** such as `{ 'is-disabled': disabled }` adds each key whose value is truthy. An **array** such as `['btn', `btn--${variant}`]` adds each entry, and entries can themselves be objects or nested arrays; empty strings and falsy object values add nothing. A static `class="btn"` and a `:class` on the same element are merged, not overwritten. For a button, the usual shape is a static base class plus a `computed` that returns an array: the variant and size classes as strings, and an object for boolean states like `is-disabled` and `is-loading`. Vue normalises all of it into one class string and updates the element when the reactive inputs change.

code

vue · 14 lines
vue
<script setup lang="ts">
import { ref } from 'vue'

const isActive = ref(true)
const hasError = ref(false)
const size = ref<'sm' | 'lg'>('sm')
</script>

<template>
  <!-- renders class="tab active" -->
  <div class="tab" :class="{ active: isActive, 'text-danger': hasError }">Tab</div>
  <!-- renders class="btn--sm" -->
  <div :class="['btn--' + size, { 'text-danger': hasError }]">Mixed</div>
</template>

go deeper

for a junior

Recall the three forms — string, object toggled by truthiness, array of either — and that :class merges with a static class.

for a middle

Explain how Vue normalises nested arrays and objects into one string, and why a computed returning an array suits enumerated plus boolean props.

for a senior

Keep class logic in one tested computed, avoid string concatenation bugs like 'btn--undefined', and separate root-class merging from the parent.

for a principal

Choose a class-naming contract for the design system's components so variants and states map to classes predictably across teams.

## Why class gets special treatment Every attribute can be bound with `v-bind` (shorthand `:`), but building a class string by concatenation is error-prone: stray spaces, leftover `undefined`, classes that should disappear but do not. So Vue 3 accepts **three value types** for `:class`: - a **string**, used as-is: `:class="'btn btn--primary'"`; - an **object**, where each key is a class name that is added when its value is **truthy**: `:class="{ 'is-disabled': disabled }"`; - an **array**, where each entry is normalised in turn and may itself be a string, an object or another array: `:class="['btn--' + size, { 'is-loading': loading }]"`. Vue flattens whatever you give it into one space-separated string (a helper called `normalizeClass`) before touching the DOM. ## Static class plus :class A plain `class` attribute and a `:class` binding on the same element **coexist**. The Vue guide's example renders `class="static active"` from `class="static"` and `:class="{ active: isActive }"`. So the idiom is: - put the classes every instance always has in the static attribute; - put everything that depends on state in `:class`. ## The button, built step by step The component takes a `variant` (`primary`, `ghost`, `danger`), a `size` (`sm`, `md`, `lg`), and `disabled` and `loading` flags. ```vue <script setup lang="ts"> import { computed } from 'vue' const props = defineProps<{ variant: 'primary' | 'ghost' | 'danger' size?: 'sm' | 'md' | 'lg' disabled?: boolean loading?: boolean }>() const classes = computed(() => [ `btn--${props.variant}`, `btn--${props.size ?? 'md'}`, { 'is-disabled': props.disabled, 'is-loading': props.loading }, ]) </script> <template> <button class="btn" :class="classes" :disabled="disabled || loading"> <slot /> </button> </template> ``` For `variant="danger" size="sm" loading`, the element renders `class="btn btn--danger btn--sm is-loading"`. When `loading` turns false, Vue recomputes the array and removes `is-loading`; nothing else changes. Why this shape works well: 1. **Enumerated props become strings** in the array — one class per value, no `if` chain. 2. **Boolean props become object keys**, which is exactly what the object syntax is for. 3. **A `computed`** keeps the template short and caches the result until one of the props it read changes. ## Object, array, or both? | Need | Best fit | Example | |---|---|---| | a few on/off classes | object | `{ active: isActive, 'text-danger': hasError }` | | classes named by a value | array of strings | `[activeClass, errorClass]` | | a mix of the two | array containing an object | `[size, { 'is-disabled': disabled }]` | | one conditional class in a list | ternary in an array | `[isActive ? activeClass : '', errorClass]` | The guide notes that ternaries get verbose with several conditions, which is why an object inside an array is usually cleaner. ## Mistakes interviewers listen for - **Quoting keys wrongly.** A class name with a hyphen must be a quoted key: `{ 'text-danger': hasError }`. - **Expecting falsy array entries to vanish only when they are `false`.** Vue skips any entry that normalises to an empty string, including `''`, `null`, `undefined` and `false`, but a string like `'undefined'` from careless concatenation (`'btn--' + undefined`) is added literally. - **Rebuilding the string by hand.** String concatenation throws away the object and array syntax that make classes toggle cleanly. - **Believing `:class` replaces the static attribute.** They merge. How classes set on the **component** by its parent are merged onto this button's root element is a separate topic — fallthrough attributes — and follows its own rules.

  • In Vue 3, does a :class binding replace the element's static class attribute?
    No. Vue merges them: `class="btn"` plus `:class="{ active: isActive }"` renders `btn active` when `isActive` is truthy. Put invariant classes in the static attribute and state-dependent ones in the binding.
  • Why bind :class to a computed rather than write the object inline for a Vue 3 button with several states?
    A `computed` keeps the template readable, gives the class logic a name you can test, and caches the result until one of the reactive values it read changes. The inline form works the same at runtime, but long objects in templates are hard to review.

The object syntax is a panel of labelled light switches: each class name is a label, and the class is on exactly when its switch is truthy. The array syntax is a list of panels and fixed lamps that Vue wires together into one row.

saying these in an interview costs you the question

  • A :class binding overwrites the static class attribute on the same element.
  • Object syntax adds a class only when its value is exactly true.
  • Arrays passed to :class cannot contain objects.
  • 'btn--' + undefined is dropped automatically because it is falsy.
  • You must join class names into one string before binding them.