A Vue 3 template binds an object to an element and it arrives as "[object Object]", while another binding never shows in the markup — how does v-bind choose property or attribute, and when do .prop and .attr help?
answer
- an in check on the element
- properties hold any type
- attributes are always strings
- force with a modifier
basics
~20 sOn an element, Vue 3's v-bind sets a DOM property when the key exists on the element and an attribute otherwise, with a few forced exceptions. .prop forces a property, .attr an attribute, and .camel camelizes names in in-DOM templates.
solid answer
~50 sFor a plain element, Vue checks `key in el`: if the element has such a property, Vue assigns the **property**, which keeps objects and arrays intact; otherwise it calls `setAttribute`, which **stringifies** the value. So an object bound to a name the element does not define as a property — typically a custom element that has not been upgraded yet — becomes `"[object Object]"`. `.prop` (shorthand `.name`) forces a property and `.attr` forces an attribute; both exist since 3.2. The reverse surprise is that property bindings do not always show up as attributes in the markup, which matters for CSS attribute selectors and some third-party scripts. Vue also forces a few names to attributes regardless — `form`, `list` on inputs, `width`/`height` on media elements, `spellcheck`, `draggable`. `.camel` only renames kebab-case to camelCase for in-DOM templates, such as SVG `viewBox`.
go deeper
Know that attributes are strings while DOM properties can hold any value, and that v-bind usually picks the property when one exists.
Explain the in check, the forced-attribute exceptions, and what .prop, .attr and .camel change.
Diagnose [object Object] attributes and invisible property bindings, and fix them with .prop or .attr rather than manual DOM code.
Define how the design system passes rich data to custom elements — properties by contract, attributes for styling hooks — so framework wrappers stay thin.
## Attribute or property? Every HTML element has **attributes** (strings in the markup, read with `getAttribute`) and **DOM properties** (JavaScript fields on the element object). Many are linked — setting `input.value` or `a.href` affects what you see — but they are not the same: a property can hold any JavaScript value, while an attribute is always a string. When Vue 3 applies `v-bind` to a plain element, it must pick one. ## Vue's default rule The API reference describes it: Vue checks whether the element has the key defined as a property using an `in` check; if it does, Vue sets the DOM property, otherwise an attribute. The runtime adds a short list of **forced attributes**, for cases where the property is read-only, reflects badly, or behaves differently: - `form` on form controls (the property is read-only); - `list` on `<input>`; - `type` on `<textarea>`; - `width` and `height` on `<img>`, `<video>`, `<canvas>`, `<source>`; - `spellcheck`, `draggable`, `translate`, `autocorrect` (their properties are booleans, so the string `"false"` would turn them on); - `sandbox` on `<iframe>`; - anything on SVG elements, apart from `innerHTML`, `textContent` and function-valued native `on*` handlers. `class` and `style` have their own patchers, and `on*` keys are event listeners. ## The two symptoms 1. **`[object Object]` in an attribute.** A value can only survive intact as a property. If the name does not exist on the element when Vue patches it — the usual case is a custom element whose class has not been registered yet — Vue falls back to `setAttribute`, which stringifies. **Fix:** `:config.prop="settings"` or its shorthand `.config="settings"`. 2. **A binding that never appears in the markup.** When Vue sets a property that does not reflect to an attribute, nothing shows up in DevTools' Elements panel or in `outerHTML`, and CSS such as `[aria-busy]` or `[data-state]` cannot match it. **Fix:** `:foo.attr="value"` forces `setAttribute`. ## The modifiers | Modifier | Effect | Typical use | |---|---|---| | `.prop` (shorthand `.`) | always assign a DOM property | objects or arrays for custom elements; `innerHTML`-style properties | | `.attr` | always call `setAttribute` / `removeAttribute` | values that CSS selectors or external scripts read from the markup | | `.camel` | camelize a kebab-case argument | in-DOM templates, where the browser lowercases attribute names: `:view-box.camel` on `<svg>` | `.prop` and `.attr` were added in Vue 3.2. `.camel` is unnecessary in single-file components or any pre-compiled template, because those keep the case you wrote. ## A worked example on the button component The design system's button wraps a `<ds-icon>` custom element that expects an `options` object and exposes a `size` attribute the stylesheet selects on. ```vue <script setup lang="ts"> const iconOptions = { name: 'spinner', spin: true } defineProps<{ size: 'sm' | 'md' | 'lg' }>() </script> <template> <button class="btn"> <ds-icon .options="iconOptions" :size.attr="size" /> <slot /> </button> </template> ``` - `.options` keeps the object intact whether or not `ds-icon` has been defined yet. - `:size.attr` guarantees `size="sm"` appears in the markup, so `ds-icon[size="sm"]` in CSS matches. To stop Vue from first trying to resolve `ds-icon` as a Vue component — and warning when it cannot — configure it as a custom element in the compiler options, a separate setup step. ## How to diagnose - Inspect the element in the console: `el.options` (property) versus `el.getAttribute('options')` (attribute). - If the attribute reads `[object Object]`, the binding went down the attribute path; add `.prop`. - If the property is set but CSS does not match, the value never became an attribute; add `.attr`.
- In Vue 3, when do you need the .camel modifier on v-bind?Only for in-DOM templates, where the browser's HTML parser lowercases attribute names before Vue sees them. `:view-box.camel="viewBox"` on an `<svg>` restores the `viewBox` name. Single-file components and other pre-compiled templates keep the case you wrote, so they never need it.
- In Vue 3, why is width on an <img> always set as an attribute even though img.width exists as a property?Vue's runtime forces `width` and `height` on `<img>`, `<video>`, `<canvas>` and `<source>` to the attribute path, because assigning the numeric DOM property does not behave the same as writing the attribute you authored. Setting the attribute keeps exactly the value in the template.
saying these in an interview costs you the question
- Vue always sets v-bind values as HTML attributes.
- .camel is needed for camelCase attributes in single-file components.
- A property binding always shows up as an attribute in the markup.
- Objects bound with v-bind are serialised to JSON attributes.
- Vue sets a property whenever the value is an object, and an attribute otherwise.