In Vue 3, how do v-bind="obj" and the 3.4 same-name shorthand work, and which wins when obj and an attribute share a key?
answer
- no argument means an object
- declaration order decides
- class and style are merged
- :id means :id="id"
basics
~20 sv-bind without an argument spreads an object's keys as attributes or props. In Vue 3 the later declaration wins on a clash, except class and style, which merge. Since 3.4, :id alone is shorthand for :id="id".
solid answer
~40 s`v-bind="attrs"` binds every key of the object, as if each were written separately. When the same key also appears as its own attribute, Vue 3 merges the bindings **in declaration order**, so the later one wins: `<div id="red" v-bind="{ id: 'blue' }">` renders `id="blue"`, and swapping them renders `red`. That changed from Vue 2, where the explicit attribute always won. `class` and `style` are the exception — they are **combined**, not overwritten — and listeners under the same `on*` key both run. The **same-name shorthand** (Vue 3.4+) drops the value when it matches a variable: `:id` means `:id="id"`, and kebab-case names are camelized, so `:aria-label` binds `ariaLabel`. The trap: `<button :disabled>` binds a variable named `disabled`; it does not mean "disabled is present".
code
vue · 11 lines<script setup lang="ts">
const props = defineProps<{ variant: 'primary' | 'ghost'; disabled?: boolean }>()
const defaults = { type: 'button', class: 'btn' }
</script>
<template>
<!-- defaults first, then the component's own values win -->
<button v-bind="defaults" :class="`btn--${props.variant}`" :disabled>
<slot />
</button>
</template>go deeper
Recall that v-bind without an argument spreads an object, and that :id alone means :id="id" in Vue 3.4 and later.
Explain the Vue 3 declaration-order rule, the class, style and listener merging exceptions, and camelized shorthand names.
Catch Vue 2-era templates that relied on explicit attributes winning, and the :disabled shorthand silently binding undefined.
Decide how wrapper components layer defaults, caller attributes and forced values so priority is visible in the template order.
## v-bind without an argument `v-bind` normally takes an argument: `:id="panelId"`. Without one, it takes an **object** and binds each key: ```vue <script setup lang="ts"> const attrs = { id: 'save', type: 'submit', 'aria-describedby': 'save-help' } </script> <template> <button v-bind="attrs">Save</button> </template> ``` This renders `<button id="save" type="submit" aria-describedby="save-help">`. On a component, the same object would bind declared props and pass the rest along as attributes. ## Who wins on a clash When an element has both `v-bind="obj"` and an individual attribute for the same key, Vue 3 merges the bindings **in the order they are declared**. The Vue 3 migration guide documents this as a breaking change: | Template | Vue 3 result | Vue 2 result | |---|---|---| | `<div id="red" v-bind="{ id: 'blue' }">` | `id="blue"` | `id="red"` | | `<div v-bind="{ id: 'blue' }" id="red">` | `id="red"` | `id="red"` | In Vue 2 the individual attribute always overrode the object. In Vue 3 **the later declaration wins**, so you choose the priority by position: - write **defaults** in the object and **overrides** after it: `v-bind="defaults" type="button"`; - write **fixed values** first and let the object override them: `type="button" v-bind="overrides"`. ## Keys that merge instead of overriding Three kinds of key do not follow "last wins": 1. **`class`** — both values are normalised and joined: `class="btn" v-bind="{ class: 'btn--primary' }"` gives `class="btn btn--primary"`. 2. **`style`** — both values are merged, later properties winning per property. 3. **Event listeners** (`onClick` and friends) — both handlers are kept and both run. This is why a wrapper can spread caller-supplied attributes onto the button without wiping its own base class. ## The same-name shorthand (3.4+) Vue 3.4 added a shorthand for the very common case where the attribute and the variable share a name: ```vue <script setup lang="ts"> const id = 'save' const ariaLabel = 'Save draft' </script> <template> <!-- same as :id="id" :aria-label="ariaLabel" --> <button :id :aria-label>Save</button> </template> ``` Details worth knowing: - The argument is **camelized** to find the variable: `:aria-label` reads `ariaLabel`. - It works with the long form too: `v-bind:id`. - Only a static argument is allowed; a dynamic `:[key]` cannot use it. - It is Vue 3.4+; older versions do not support it. ## The :disabled trap In plain HTML, `<button disabled>` means "disabled". With the shorthand, **`<button :disabled>` means `:disabled="disabled"`** — a binding to a variable called `disabled`. If the component has a `disabled` prop, that is exactly right. If no such variable exists, the expression is `undefined` (development builds warn that the property was accessed during render but is not defined), and the attribute is omitted, so the button is enabled. Drop the colon for a static boolean attribute. ## When to reach for which | Situation | Use | |---|---| | attribute name equals the variable name | same-name shorthand `:id` | | a bag of optional attributes from props or config | `v-bind="attrs"` | | one attribute must always win | place it after the object | | caller classes plus base classes | rely on class merging | How a component's `$attrs` fall through to its root element automatically is a separate mechanism with its own merge rules.
- In Vue 3.4+, what does <button :disabled> bind if the component has no variable named disabled?It binds `undefined`, because `:disabled` is shorthand for `:disabled="disabled"`. A boolean attribute bound to `undefined` is omitted, so the button is enabled. Write `disabled` without the colon for a static attribute.
- In Vue 3, how do you keep a component's base class when spreading caller attributes with v-bind?Nothing special is needed: `class` values from an explicit attribute and from a `v-bind` object are merged, not overwritten, so `class="btn" v-bind="callerAttrs"` keeps `btn` and adds the caller's classes. Only ordinary keys like `id` or `type` follow the later-wins rule.
Declaration order works like layers of paint: whatever is applied later covers the same spot. Class and style are stickers instead of paint — each new sheet adds its stickers, and for style a later sticker covers an earlier one only where they overlap.
saying these in an interview costs you the question
- In Vue 3 an explicit attribute always overrides the same key in v-bind="obj".
- A class key in a v-bind object replaces the element's class attribute.
- <button :disabled> in Vue 3.4 is a static boolean attribute.
- The same-name shorthand keeps kebab-case names, so :aria-label reads aria-label.
- v-bind="obj" can only be used on components, not on native elements.