Use Fallthrough Attribute Inheritance in Vue 3 Components

Robin
Updated on June 12, 2023

When we use components in Vue 3, we can pass information from one component to another through props. For that, you have to specify explicitly the props in your component.

There are some fallthrough attributes that every component can receive without specifying them as props like class, style, etc.

When you set these fallthrough (non-prop) attributes to your component, Vue automatically adds those to the root element of that component. This is known as attribute inheritance in Vue 3 components.

In this article, we will discuss everything about attribute inheritance behavior, how it works, how to control this behavior, and many more in detail.

What is Attribute Inheritance in Vue 3?

Attribute inheritance in Vue 3 refers to the behavior of passing non-prop attributes from a parent component to its child components. By default, Vue 3 will add those attributes to the root element of the chile component.

But you can change this default behavior very easily and attach the non-prop attributes to any HTML element inside your component.

You just have to bind $attrs property to that element using the v-bind="$attrs" directive. If you don't know the differences between prop and non-prop attributes, it is necessary to understand that.

Also Read: How to Pass Data From Child to Parent in Vue 3 (Composition API)


Prop Attributes and Non-prop(Fallthrough) Attributes

In Vue 3, there are two types of attributes that can be passed to components: prop attributes and non-prop attributes (formerly known as fallthrough attributes). Let's take a look at each type:

  • Prop Attributes: Prop attributes are explicitly defined properties in the component that are meant to accept values from the parent component. These attributes are declared in the component's props option or using defineProps() method in the new composition API.
  • Non-prop (Fallthrough) Attributes: Non-prop attributes, also known as fallthrough attributes in Vue, are attributes set on a component but not explicitly defined as props. These attributes are meant to be inherited by child components. The most commonly used fallthrough attributes are class, id, style.

Default Fallthrough Attribute Inheritance in Vue 3 Components

You have learned about attribute inheritance and non-prop attributes. Now let's see how both of them work together in a Vue 3 component.

Suppose you have a child component called MyInput.vue in your project. This component accepts a title prop to set the <input> value.

MyInput.vue Component:

          <template>
    <div>
        <input
            type="text"
            class="input-group"
            placeholder="Username"
            :value="title"
        />
    </div>
</template>

<script setup>
const props = defineProps({
    title: String,
})
</script>
        

When you use this component in any other component, you can pass this title prop. This is a prop attribute because you have declared it as a prop in your component.

App.vue Component:

          <template>
    <main>
        <MyInput title="Hello world" />
    </main>
</template>

<script setup>
import MyInput from './components/MyInput.vue'
</script>
        

But other than this title attribute, you can also pass other attributes to this MyInput component. Then those attributes will become fallthrough attributes.

App.vue Component:

          <template>
    <main>
        <MyInput title="Hello world" class="input-text" />
    </main>
</template>

<script setup>
import MyInput from './components/MyInput.vue'
</script>
        

Here the class attribute is not declared as the prop inside MyInput.vue component. Therefore, it is a fallthrough attribute. Vue will add this attribute to the root element of this component automatically using attribute inheritance.

HTML of the MyInput.vue component will be like this:

          <div class="input-text">
    <input type="text" class="input-group" placeholder="Username">
</div>
        

The root element of the MyInput.vue component is div that's why class="input-text" attribute is added to this div element automatically.

You can set as many non-prop attributes as you want to your components. All of them will be added to the root element. You can also change this default behavior.

That means it is possible to choose a specific element in your component where you want to add your fallthrough attributes. But first, you need to disable the default behavior.

Also Read: Composition API VS Options API in Vue 3 For Beginners


How to Disable Default Attribute Inheritance in Vue 3

In Vue 3, you can disable attribute inheritance by setting the inheritAttrs option to false in the component. You can set this option to false in 2 ways.

First, add a dedicated <script> tag to your component and export a default object. Define the inheritAttrs: false property in that object.

          <template>
    <div>
        <input
            type="text"
            class="input-group"
            placeholder="Username"
            :value="title"
        />
    </div>
</template>

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

<script setup>
const props = defineProps({
    title: String,
})
</script>
        

Second, if you don't want to use an additional <script> tag in your component, you have defineOptions() method. Call this method and pass an object as an argument with the inheritAttrs: false property.

This method is directly accessible in a Vue component. You don't have to import it.

          <template>
    <div>
        <input
            type="text"
            class="input-group"
            placeholder="Username"
            :value="title"
        />
    </div>
</template>

<script setup>
defineOptions({
    inheritAttrs: false,
})

const props = defineProps({
    title: String,
})
</script>
        

Now when you use this MyInput.vue component in any other component and pass non-prop attributes to it, Vue won't add them to the root element (in this case <div>) of your MyInput.vue component.

App.vue Component:

          <template>
    <main>
        <MyInput title="Hello world" class="input-text" style="padding: 4px 6px" />
    </main>
</template>

<script setup>
import MyInput from './components/MyInput.vue'
</script>
        

HTML of the MyInput.vue component will be like this:

          <div>
    <input type="text" class="input-group" placeholder="Username">
</div>
        

As you can see, the root <div> element doesn't have the class and style attributes. You have successfully disabled the default attribute inheritance behavior.

By explicitly disabling the automatic inheritance of non-prop attributes from the parent to the child component, you get more control over attribute handling and can add non-props attributes in any element you want.

Also Read: How to Use Vue 3 v-model Directive in Custom Components


How to Use Fallthrough Attributes in Vue 3 Components

In the previous section, you have seen how to disable the default attribute inheritance in Vue 3. Now you can explicitly define in which element the fallthrough attributes should go.

There are 2 ways to add fallthrough attributes in a specific element:

  • You can add any specific attribute from the $attrs property.
  • You can add all non-prop attributes in an element using v-bind="$attrs" (binding $attrs property).

Let's see some examples of these techniques.


Add Attributes in a Custom Element

In our MyInput.vue component, the root element is a <div> but you want to add the class attribute to the <input> element.

MyInput.vue Component (Adding a specific attribute):

          <template>
    <div>
        <input
            type="text"
            :class="`input-group ${$attrs.class}`"
            placeholder="Username"
            :value="title"
        />
    </div>
</template>

<script setup>
defineOptions({
    inheritAttrs: false,
})

const props = defineProps({
    title: String,
})
</script>
        

You can access all the non-prop attributes from the $attrs object. I am only adding the class attribute in this component. If you pass any other non-prop attributes like style, id, etc, Vue will ignore them.

App.vue Component:

          <template>
    <main>
        <MyInput title="Hello world" class="input-text" style="padding: 4px 6px" />
    </main>
</template>

<script setup>
import MyInput from './components/MyInput.vue'
</script>
        

Output HTML will look like this:

          <div>
    <input type="text" class="input-group input-text" placeholder="Username">
</div>
        

I have passed class and style attributes to the MyInput.vue component but rendered HTML only has the class attribute. It contains 2 class names.

The <input> element already had input-group class name in the component. When you pass another class attribute from App.vue component, Vue will join both class attributes together. This is known as "Attribute Merging".

That's why the rendered <input> element has both input-group and input-text class names.

App.vue Component:

          <template>
    <main>
        <MyInput title="Hello world" style="padding: 4px 6px" />
    </main>
</template>

<script setup>
import MyInput from './components/MyInput.vue'
</script>
        

Output HTML will look like this:

          <div>
    <input type="text" class="input-group undefined" placeholder="Username">
</div>
        

It is very important to note that, as you are adding a specific attribute to the MyInput.vue component, the class attribute value in the rendered <input> element will show undefined if you don't pass a class attribute to the MyInput.vue component.

That's why it is better to add all attributes to an element by binding the $attrs object.

MyInput.vue Component (Adding all attributes):

          <template>
    <div>
        <input
            type="text"
            class="input-group"
            placeholder="Username"
            :value="title"
            v-bind="$attrs"
        />
    </div>
</template>

<script setup>
defineOptions({
    inheritAttrs: false,
})

const props = defineProps({
    title: String,
})
</script>
        

Here I have added the v-bind="$attrs" to the <input> element in the MyInput.vue component. This will add all non-prop attributes to this <input> element.

App.vue Component:

          <template>
    <main>
        <MyInput title="Hello world" class="input-text" style="padding: 4px 6px" />
    </main>
</template>

<script setup>
import MyInput from './components/MyInput.vue'
</script>
        

Output HTML will look like this:

          <div>
    <input type="text" class="input-group input-text" placeholder="Username" style="padding: 4px 6px;">
</div>
        

As you can see, the <input> element contains both class and style attributes in the rendered HTML.

Also Read: How to Style Slot Elements in Vue 3 Using Slotted Selectors


Add Attributes in a Component with Multiple Roots

Vue 3 component supports multiple root elements. By default, attribute inheritance adds fallthrough attributes to the root element.

But when you have multiple root elements in your components, you have to explicitly bind the $attrs object to a specific element. otherwise, Vue will show a warning in the console.

MyInput.vue Component:

          <template>
    <input
        type="text"
        class="input-group"
        placeholder="Username"
        :value="title"
        v-bind="$attrs"
    />
    <button>Submit</button>
</template>

<script setup>
const props = defineProps({
    title: String,
})
</script>
        

This time I have added 2 root elements in the MyInput.vue component. To make sure that your component works properly, you have to add v-bind="$attrs" to one of your root elements.

Here I am binding $attrs object with the <input> element. This means, all the non-props attributes will be added to this element.

Note: When your component has multiple root element, there is no need to remove the default attribute inheritance using the defineOptions() method.

Also Read: Best Way to Re-render Vue 3 Components: Master Guide


How to Access Non-props Attributes in Script

You can access fallthrough attributes and their values inside your component <script> tag. Composition API has 2 ways to get those attributes.

If you are using <script setup> syntax in your Vue 3 component, you have to call the useAttrs() API.

          <script setup>
import { useAttrs } from 'vue'

const props = defineProps({
    title: String,
})

const attrs = useAttrs()
console.log(attrs)
</script>
        

But if you use setup() function in your Vue 3 component, you will find fallthrough attributes in the context object:

          <script>
export default {
    props: {
        title: String,
    },
    setup(props, ctx) {
        const attrs = ctx.attrs
        console.log(attrs)
    },
}
</script>
        

Note: These fallthrough attributes are not reactive. That means it isn't possible to use watchers to track the changes or do something when their value changes.

Also Read: Context Argument in Vue 3 Composition API Script Setup


Event Listener Inheritance in Vue 3

So far we have discussed how Vue components use inheritance to pass non-prop attributes from parent to child components. It is also possible to pass event listeners.

If you add an event listener to a child component, Vue will add that to the root element of that component by default same as attributes.

MyButton.vue Component:

          <template>
    <button>Submit</button>
</template>
        

I have a MyButton.vue component where the root element is a button. When I use this component and add an event listener, like click event, it will be added to this <button> element.

App.vue Component:

          <template>
    <main>
        <MyButton @click="handleSubmit" />
    </main>
</template>

<script setup>
import MyButton from './components/MyButton.vue'

const handleSubmit = () => {
    console.log('Submitted from App')
}
</script>
        

I am adding a click event to the MyButton component. When I click the button, this handleSubmit() function will execute.

But what will happen if you also add a click event to the <button> element in MyButton.vue component?

MyButton.vue Component:

          <template>
    <button @click="onSubmit">Submit</button>
</template>

<script setup>
const onSubmit = () => {
    // handle submit
    console.log('Submitted from MyButton')
}
</script>
        

In this case, when you click the button both click event will fire. Therefore, both handleSubmit() and onSubmit() functions will run.

You can also use v-bind="$attrs" to add event listeners to a specific HTML element or if you have multiple root elements in your component.

MyButton.vue Component:

          <template>
    <div>
        <button v-bind="$attrs" @click="onSubmit">Submit</button>
    </div>
</template>

<script setup>
defineOptions({
    inheritAttrs: false,
})

const onSubmit = () => {
    // handle submit
    console.log('Submitted from MyButton')
}
</script>
        

The root element is a <div> in this component. Therefore, I am binding $attrs object to the <button> element so that any event listeners get added to it.

Don't forget to set inheritAttrs: false option. Because you have to disable the default inheritance behavior in your components.

Also Read: Improve Performance in Vue 3 Using Lazy Loading and Dynamic Import


Attribute Inheritance in Nested Components

There is another concept called inheritance in nested components in Vue. This happens when a component renders another component as its root.

Suppose you have 2 components: MyButton.vue and BaseButton.vue. The BaseButton.vue component is rendering MyButton.vue as the root element.

MyButton.vue Component:

          <template>
    <div>
        <button v-bind="$attrs" @click="onSubmit">Submit</button>
    </div>
</template>

<script setup>
defineOptions({
    inheritAttrs: false,
})

const onSubmit = () => {
    // handle submit
    console.log('Submitted from MyButton')
}
</script>
        

BaseButton.vue Component:

          <template>
    <MyButton />
</template>
        

Now if you use this BaseButton.vue component in other components and pass any attributes or events to it, Vue will automatically pass them to the <MyButton /> component.

Because here you are using <MyButton /> component as the root element.

App.vue Component:

          <template>
    <BaseButton class="btn" @click="handleSubmit" />
</template>
        

Here I am adding class attribute and click event to the BaseButton.vue component. Vue will again pass them to the MyButton.vue component. That's why it is called inheritance in nested components.

Also Read: Style Child Components & Dynamic HTML Using Vue 3 Deep Selector


Conclusion

Attribute inheritance helps Vue components to share data between components. That's why it is important to understand how it works and how you can control its default behavior.

By default, attribute inheritance will set fallthrough attributes in the root element. But you can also customize it using v-bind="$attrs" in your component.

This inheritance also works for event listeners. You can pass any event listeners from parent to child components. It is also possible to get those attributes inside your component <script setup> tag.

By understanding attribute inheritance in Vue 3, developers can effectively manage non-prop attribute passing between components.

Related Posts