Non-Prop Attributes in Vue

February 22, 2021

During the implementation of a custom component, I explored the documentation to learn about a behavior that previously seemed a bit magical to me: Non-Prop Attributes.

Props in Vue

props are one of the ways you can implement component communication in Vue. It is pretty similar to React and provides an API to express the properties such as type, required, and default.

As an example, let's look at an input component that receives a prop named value.

// BaseInput.vue
<template>
  <div>
    <input v-bind="$attrs" :value="value">
  </div>
</template>

<script>
export default {
  name: 'BaseInput',
  props: {
    value: {
      type: String,
      required: true,
    },
  },
}
</script>

And here's a possible usage:

// MyComponent.vue
<template>
  <BaseInput :value="foo" />
</template>

Non-Prop Attributes

But what if you do this?

// MyComponent.vue
<template>
  <BaseInput :value="foo" :readonly="isReadOnly" />
</template>

Here readonly is not defined in the props definition of BaseInput:

// BaseInput.vue
<script>
export default {
  // ...
  props: {
    value: {
      type: String,
      required: true,
    },
    // No definition for readonly
  },
  // ...
}
</script>

So what happens with it?

In such cases, Vue adds these attributes to the $attrs object accessible as an instance property. In the example above, readonly gets added to the object as $attrs.readonly:

{
  "readonly": true
}

Attribute Inheritance

Attribute inheritance (and disabling it) can leverage the usage of Non-Prop attributes. By default, attributes are added to the root element of your template. For example:

// BaseInput.vue
<template>
  <div>
    <input type="text" />
  </div>
</template>
// MyComponent.vue
<BaseInput data-testid="my-component-id" />

Will result in:

<div data-testid="my-component-id">
  <input type="text">
</div>

Using inheritAttrs: false, we can opt-out of this default behavior to tell Vue that we are explicitly going to bind the attributes where we need to. In the example above, we may want to set the input element's attributes rather than the outer div. To do that, we add v-bind="$attrs" to the input element.

// BaseInput.vue
<template>
  <div>
    <input type="text" v-bind="$attrs" />
  </div>
</template>

<script>
export default {
  inheritAttrs: false,
  // ...
}
</script>

Now, <BaseInput data-testid="my-component-id" /> will result in:

<div>
  <input type="text" data-testid="my-component-id">
</div>

That is especially useful when all the possible attribute keys are either unknown beforehand or tedious to manually define as a prop. For instance, an input element can have a multitude of different attributes such as value, disabled, type, readonly, and more. Another example would be to add data-testid (or other data attributes) for testing purposes or integration with 3rd party libraries.

Note: In Vue 2, the class and style attributes are not added to $attrs. That means that despite using inheritAttrs: false, these two attributes are added to the root element no matter what. This behavior changed in Vue 3 by adding them to $attrs as well.