Binding Attributes

v-bind and its colon shorthand: dynamic attributes, boolean attributes, whole objects, and class and style bindings.

Syntaxv-bind:attr="expr" | :attr="expr" | :attr (3.4+) | v-bind="objectOfAttrs"

Interpolation only prints text. To make an attribute dynamic you bind it with v-bind, whose argument is the attribute name and whose value is an expression: v-bind:href="url". The shorthand :href="url" compiles to the same code, and is what most code uses. Since Vue 3.4 there is a same-name shorthand too: :id on its own means :id="id".

Boolean attributes and objects

Boolean attributes are handled for you. :disabled="busy" adds the attribute when busy is truthy or an empty string, and removes it entirely when the value is false, null or undefined. There is no disabled="false" in HTML, so Vue does not write one.

v-bind with no argument takes an object and applies every key as an attribute: v-bind="linkAttrs".

class and style

These two accept more than strings. :class takes an object, where each truthy value adds that key as a class, or an array of names and objects. A static class attribute on the same element is merged in, not replaced. :style takes an object keyed by CSS property, in camelCase or in quoted kebab-case, or an array of such objects merged left to right.

Vendor prefixes are added automatically where a property needs them. A class set on a component lands on its root element, merged with whatever classes that root already has.

Example

Example · vue
<!-- SaveButton.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const id = 'save-button'
const busy = ref(false)
const invalid = ref(false)
const accent = ref('#2563eb')

const docsLink = {
  href: 'https://vuejs.org/guide/essentials/template-syntax.html',
  target: '_blank',
  rel: 'noopener'
}
</script>

<template>
  <div class="row">
    <!-- :id is the 3.4+ same-name shorthand for :id="id" -->
    <button
      :id
      type="button"
      class="btn"
      :class="{ 'is-busy': busy, 'is-invalid': invalid }"
      :style="{ borderColor: accent, opacity: busy ? 0.6 : 1 }"
      :disabled="busy"
    >
      {{ busy ? 'Saving' : 'Save' }}
    </button>

    <!-- one object, three attributes -->
    <a v-bind="docsLink">Docs</a>
  </div>
</template>

<style scoped>
.btn {
  border: 1px solid;
  padding: 0.4rem 0.8rem;
}

.is-busy {
  cursor: progress;
}

.is-invalid {
  color: #b91c1c;
}
</style>

When to use it

  • Disable a submit button while a save is in flight.
  • Add an active class to the current item in a navigation list.
  • Set a bar's width from a number in your data.
  • Pass a caller-supplied set of link attributes straight through to an anchor.

More examples

Object, array, and the two mixed

Three forms that produce class lists, all on elements that also carry a static class. The object form is the one to default to when the names are known and the conditions vary. The array form fits when the names themselves are data, as levelClass is here. Mixing them covers the case where a class name is computed and a condition decides whether it applies. Note the empty string in the last ternary: a falsy entry in the array contributes nothing.

Example · vue
<!-- StatusPill.vue -->
<script setup lang="ts">
const props = defineProps<{
  level: 'info' | 'warn' | 'error'
  compact: boolean
}>()

const levelClass = `pill--${props.level}`
</script>

<template>
  <!-- object: each truthy value adds its key -->
  <span class="pill" :class="{ 'pill--compact': props.compact }">A</span>

  <!-- array: each entry is a class name -->
  <span class="pill" :class="[levelClass, 'pill--bordered']">B</span>

  <!-- mixed: an object inside an array, with a computed key -->
  <span class="pill" :class="[{ [levelClass]: true }, props.compact ? 'pill--compact' : '']">C</span>
</template>

<style scoped>
.pill { border-radius: 999px; padding: 0.1rem 0.6rem; }
.pill--compact { padding: 0 0.3rem; }
.pill--bordered { border: 1px solid currentColor; }
.pill--info { background: #e0f2fe; }
.pill--warn { background: #fef9c3; }
.pill--error { background: #fee2e2; }
</style>

Styles from data, and prefix fallbacks

Reach for :style when the value is a number from your data, such as a width percentage, which no fixed class can express. Keys are camelCase or quoted kebab-case, and both appear here. The array form merges objects left to right, so a shared base object can be overridden per instance. An array of values for one property, as with display, lets the browser take the last form it understands, which is how you ship a prefixed fallback.

Example · vue
<!-- ProgressBar.vue -->
<script setup lang="ts">
const props = defineProps<{
  percent: number
  height: number
}>()

const base = { display: ['-webkit-box', 'flex'], overflow: 'hidden' }
</script>

<template>
  <div class="track" :style="[base, { height: `${props.height}px` }]">
    <div
      class="fill"
      :style="{ width: `${props.percent}%`, 'transition-duration': '150ms' }"
    ></div>
  </div>
</template>

<style scoped>
.track { background: #e2e8f0; border-radius: 4px; }
.fill { background: #2563eb; height: 100%; }
</style>

Attributes a wrapper does not know about

v-bind with no argument spreads an object of attributes, which is what you want when the caller decides the set: href and target for an external link, download for a file, aria attributes for a dialog trigger. Keys are written as attribute names, so pass href rather than Href. A later static attribute of the same name wins over the object, so put the ones the caller must not override after the v-bind.

Example · vue
<!-- IconLink.vue -->
<script setup lang="ts">
const props = defineProps<{
  label: string
  linkAttrs: Record<string, string>
}>()
</script>

<template>
  <!-- every key in linkAttrs becomes an attribute on the anchor -->
  <a class="icon-link" v-bind="props.linkAttrs">
    <span aria-hidden="true">&#8599;</span>
    {{ props.label }}
  </a>
</template>

<style scoped>
.icon-link { display: inline-flex; gap: 0.25rem; }
</style>

Discussion

  • Be the first to comment on this lesson.