skip to content

In a Vue 3 template, how does `:items="list"` on a native custom element pick property or attribute, and when do `.prop` or `.attr` help?

level: middleimportance: nice to knowfreq 24%

answer

  1. attributes only carry strings
  2. an in-operator check on the element
  3. not-yet-upgraded elements fail the check
  4. modifiers force one path

basics

~20 s

Vue 3 checks the key with the in operator: if the element has that property, Vue sets it, otherwise it sets an attribute, stringifying objects. The .prop modifier forces a property; .attr forces an attribute.

solid answer

~40 s

For each binding on a plain element, Vue 3 runs a `key in el` check. If the element exposes a property with that name — a well-built custom element defines accessors for its inputs — Vue assigns the value as a DOM property, so arrays and objects arrive intact. If not, Vue calls `setAttribute`, and a non-string value is stringified, so an array arrives as a string. The check fails when the element does not define the property, or when its definition has not loaded yet and the element is still un-upgraded. `:items.prop="list"` (shorthand `.items="list"`) skips the check and always sets the property; `:theme.attr="x"` always sets an attribute, for elements that only react to attribute changes. Both modifiers exist since Vue 3.2.

code

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

const rows = ref([
  { id: 1, name: 'Ada' },
  { id: 2, name: 'Grace' },
])

function onSelect(event: Event) {
  const [row] = (event as CustomEvent<[{ id: number }]>).detail
  console.log('selected', row.id)
}
</script>

<template>
  <!-- data-grid is a native custom element; isCustomElement matches it -->
  <data-grid
    .rows="rows"
    :density.attr="'compact'"
    @row-select="onSelect"
  />
</template>

go deeper

for a junior

Know that attributes are strings, so arrays and objects must reach a custom element as properties.

for a middle

Explain Vue's in-operator check, why it can fail, and what the .prop and .attr modifiers force.

for a senior

Diagnose stringified data from lazily defined elements and decide between .prop, earlier registration, or fixing the element's accessor.

for a principal

Set a contract for the web-component libraries you adopt: properties for data, attributes for configuration, defined before the app mounts.

## Why the choice matters An HTML **attribute** is part of the markup and always holds a string. A DOM **property** is a JavaScript field on the element object and can hold anything — an array, an object, a function. Custom elements usually accept rich data through properties and simple configuration through attributes. A framework that binds values onto a custom element has to pick one, and a wrong pick silently turns `[{ id: 1 }]` into a useless string. ## Vue 3's rule When Vue patches a binding on a plain element (anything the compiler did not treat as a component), it decides per key: 1. `v-on` listeners are attached as event listeners. 2. A key written with the `.prop` modifier (compiled to a `.`-prefixed key) is set as a property; `.attr` (compiled to a `^`-prefixed key) is set as an attribute. 3. Otherwise Vue checks `key in el`, with a few special cases for native elements. If the element has the property, Vue assigns it: `el.items = list`. 4. If not, Vue sets an attribute: `el.setAttribute('items', list)`, which the browser stringifies. For a well-behaved custom element that defines an `items` accessor on its class, step 3 does the right thing and you never think about it. ## When the check fails - **The element never defines the property.** Some elements read a JSON string from an attribute, or expose data only through a method. The `in` check is false, so an object arrives as a stringified attribute. - **The element is not upgraded yet.** If the script that defines `<data-grid>` loads after Vue renders, the element is still a generic `HTMLElement` with no `items` accessor. The check fails and Vue writes an attribute. - **The accessor lives under a different name.** Vue checks the key you bound (`items`); an element that exposes `rows` fails for `items`. ## The two modifiers | Binding | What Vue does | Use it when | |---|---|---| | `:items="list"` | `in` check, then property or attribute | the element defines its properties properly | | `:items.prop="list"` or `.items="list"` | always `el.items = list` | complex data and the check can fail | | `:theme.attr="name"` | always `setAttribute('theme', name)` | the element only observes attributes, or CSS selects on the attribute | `.prop` on an un-upgraded element creates an **own property** on the instance before the class exists; whether the element picks it up after upgrade depends on how the element was written. Vue's own `defineCustomElement` elements do handle this case, reading pre-set own properties when they initialise. ## Listening to the element's events The other half of consuming a custom element is its events. `@change="onChange"` on a custom element attaches a native listener, and the handler receives the native event object. Data a custom element sends with a `CustomEvent` is read from `event.detail`. In SFC templates, a listener name containing capital letters on a plain element keeps its case, so an element that dispatches `colorPicked` can be caught with `@colorPicked`. ## Custom elements built with Vue When the element on the other side was itself made with Vue's `defineCustomElement`, Vue's renderer recognises it and sets **declared props** as properties even when the plain `in` check would not help — for example when the element wraps an async component whose props are not declared yet. For elements from any other source, the `in` check and the two modifiers are all there is, which is why the element's own documentation of its property names matters. ## Checklist - Bind primitives normally; they work either way. - For arrays and objects, confirm the element exposes the property, or use `.prop`. - If the element's definition is lazy-loaded, prefer `.prop`, or register the element before the Vue app mounts. - Use `.attr` only when the element or your CSS depends on the attribute being present. - If the element still ignores the value, check its documentation for the property name — `.prop` sets exactly the key you wrote.

  • A custom element's script is loaded lazily after the Vue app mounts, and its `rows` binding arrives as "[object Object]". Why?
    When Vue patched the binding, the element was not yet upgraded, so it had no `rows` accessor and the `key in el` check failed. Vue fell back to `setAttribute`, which stringifies an array of objects. Binding with `.rows="rows"` (the `.prop` shorthand) always sets the property, or you can register the element before the app mounts.
  • When would you force an attribute with `.attr` even though the element has a matching property?
    When something depends on the attribute being present in the DOM: the element's own styles select on `[theme=dark]`, it only reacts to attribute changes, or page CSS or tests query the attribute. Setting the property alone would change the element's state without writing the attribute those selectors look for.

saying these in an interview costs you the question

  • Vue always sets bindings on custom elements as attributes.
  • Vue serialises objects to JSON before setting them as attributes.
  • The .prop modifier works only on Vue components, not native elements.
  • Registering the tag with isCustomElement makes Vue set every binding as a property.
  • The in check succeeds before the element's definition has loaded.