Falling with Style

Falling with Style

Learn how Vue.js fall-through attributes work, when they're useful, and common pitfalls to avoid. Master class, style, and event handling in components.

Abdel Awad

Abdel Awad

October 28, 2025

Falling with Style

Fall through attributes are one of these simple features in Vue.js that you usually don’t think about much, yet there is more to them than meets the eye.

What are fall through attributes?

In Vue.js components, you usually define props which more or less act as a contract between the component and the parent. It tells the parent what kind of attributes the component expects to receive.

      <script setup>
defineProps({
  name: String,
});
</script>

    

But what happens when you pass an attribute that is not defined in the component’s props? What happens to it?

      <MyComponent name="John" unknown-attribute="value" />

    

In this case, the unknown-attribute will be passed down to the component’s root element. So in a way, the root element in the component’s template will “inherit” the unknown-attribute attribute.

This behavior isn’t limited to attributes, you can also pass down events to the component’s root element. For example, if you pass a click event to the component, the component’s root element will listen for the click event and it would work even if the component doesn’t emit a click event itself.

Why are they useful?

Fall through attributes are extremely useful in three use cases:

  • Styling: You can use fall through attributes to style the component’s root element via class or style attributes.
  • Event Listeners: You can use fall through attributes to listen for events on the component’s root element.
  • Selector Matching: Most notably the id attribute to be targeted by other accessibility attributes or data-* attributes for tests and other tools.

In these cases, fall through attributes are a better fit since you really don’t want to define those common attributes on every single component that you have.

That makes perfect sense if you think about it. If I want to add a margin to the component’s root element, I would just pass mt-4 as a class if I’m using Tailwind CSS.

You really don’t want to define a class prop on every single component that you have just to style the component’s root element, and at the same time you don’t want to define a margin-top prop on every single component that you have just to style the component’s root element.

Keep in mind class and style prop types are more complex than just a string—they can be arrays or objects as well.

      <MyComponent :class="['mt-4', { 'ring-1 ring-red-500': isError }]" />

    

So having class and style implicitly available on all components is a great way to keep your components simple, focused and flexible, and the same goes for events and other attributes.

Fall through attributes merging

A good question to ask here is what happens if the component already has a class attribute applied to the root element? What about events and other attributes?

For the class and style attributes, their values get merged with the component root element’s own attributes. So you don’t have to worry about overriding the component’s own class and style attributes, they will be merged together.

      <!-- Parent -->
<MyComponent class="mt-4" />

<!-- MyComponent.vue -->
<template>
  <div class="bg-red-500"></div>
</template>

<!-- Rendered Result -->
<div class="bg-red-500 mt-4"></div>

    

For events, they will also be merged with the root element’s own event listeners. So you can listen for the same event on the component’s root element and the component itself and both handlers will be called.

      <!-- Parent -->
<MyComponent @click="handleClick" />

<!-- MyComponent.vue -->
<template>
  <div @click="handleClick"></div>
</template>

    

This however isn’t the case for the other attributes, they will override the component’s own attributes which can be a bit surprising.

When it can go wrong

Sometimes this behavior may yield unexpected results. You may have already guessed what can go wrong here, but let me run you through a few scenarios.

Unsafe Styling

Classes and styles falling through is desirable for the most part, yet you can easily break a component design by relying on this behavior. From my experience, only a few classes or styles can be safely used as fall through attributes. Your mileage may vary, but these are the ones I have found to be safe most of the time:

  • margin
  • transform
  • opacity
  • z-index

There are probably more, but these are the ones I don’t have to worry much about when applying them to a component. You could probably add more to the list, but you get the idea.

Other styling properties can cause layout shifts, break the design, or even cause the component to not work or look as expected. I like to treat my component styling as a black box: I can move the box, I can push it to the side, I can make it less visible, I can move it through layers, but I never try to make it bigger or smaller.

Attributes overriding the component’s own attributes

With other attributes, you can easily override a desirable attribute that was already applied to the component’s root element. For example, take a look at this button component:

      <!-- ButtonComponent.vue -->
<button
  type="button"
  class="...."
>
  <slot />
</button>

    

This component explicitly creates buttons that cannot be used to submit a form since the type attribute is set to button. However if you pass a type="submit" attribute to the component, it will override the type attribute and the button will be able to submit a form.

      <!-- Parent -->
<ButtonComponent type="submit" />

    

And while this example is a bit explicit or contrived, it is possible to break a component’s design by relying on this behavior.

Attributes falling through to the wrong element

For events, it may even be a bad idea to rely on them falling through in the first place. Consider this example:

      <!-- InputField.vue -->
<div>
  <input type="text" />
</div>

    

Let’s say you want to apply a placeholder and a change event to the component, since we like fall through attributes you might be tempted to do this:

      <!-- Parent -->
<InputField placeholder="Enter your name" @change="handleChange" />

    

This wouldn’t work because fall through attributes will be applied to the root element, so the placeholder and change handler attributes will be applied to the div element instead of the input element. Neither would work.

So attributes/listeners falling through to the wrong element is a bit of a gotcha, and you should be aware of it.

Deep Fall Through

This one is a bit funny. Consider an example where a component’s root element is another component. So what happens if an attribute falls through to the nested component?

Well, the same as before. The attribute will be applied to the nested component, and here you may run into one of two situations:

  1. The fallen attribute is not explicitly defined in the nested component’s props or emits.
  2. The fallen attribute is explicitly defined in the nested component’s props or emits.

I’m not concerned about the first situation, it’s usually harmless. The second situation is a bit more interesting, and it’s the one that can cause problems.

Here’s an example:

https://play.vuejs.org/#eNqFUsFOwzAM/ZWQS0FCqxCcRpkECGlwYBMg7ZLL1LldRppEiTsmVf13nHTdpgm2W2w/2895r+GP1g7WNfAhz3zupEXmAWs7ElpW1jhkDXNQsJYVzlQsIWiyKz2bym7zgzQEYVJyL7TQudEeWeVL9hD6L5MxKGXYzDi1uEiuhM7Sbh0togChsmqOQBFjWWFcFV/0jjtQooIHwWeTmeAs7VDpFpalB938mqOn5YUsBytvNN3VBLTgOQ2SCtzEoiRygg9ZrITanLj9vMUcuhqu+3y+hPz7j/zKb0JO8KkDD24Ngu9qOHclYFd++XyHDb13xcosakXoE8UP8EbVgWMHe6r1gmgf4CLb1yiB1OWXf9kgaN8fFYgGZBvxgpMk4Q//O31P93ZwF/uEbukXezmPjcHUXJckBdKYvUnGUuPOCSHonXBK5tgUxDxWsB9wZnfnMeuM9eSyBRRSwzREWbwummbIPDr6JaLSji6vzjBa3oyapmtkbZulFB+za38BlY4S2g==

In that example, the title attribute falls through to the nested component, and it is explicitly defined in the nested component’s props. So the nested component will receive the title as a prop and will render an h1 element with the title as the text. You might have expected the title attribute to cause a tooltip to appear instead.

Multiple root elements

Lastly, what if you don’t have a single root element in the component? There are many scenarios where you can end up with multiple root elements:

1. Duh, multiple root elements

      <template>
  <div>Hello</div>
  <div>World</div>
</template>

    

In this case, Vue wouldn’t know which element to apply the attributes/listeners to.

2. Renderless Components

      <template>
  <slot />
</template>

<script setup>
// Some logic here...
</script>

    

In this case, there could be an element, there could be multiple elements, there could be no elements. Again, Vue wouldn’t know which element to apply the attributes/listeners to.

3. Dynamic Content

      <template>
  <div v-if="isFirst">First</div>
  <div v-else>Second</div>
</template>

    

Even though the component can render either one of the elements (so technically it has a single root element), Vue still wouldn’t know which element to apply the attributes/listeners to.

Luckily, in all of these cases, Vue emits a warning when fall through attributes cannot be applied to a component.

So how can we address all of these scenarios?

Disabling fall through attributes

First off, we need to disable the fall through attributes feature in Vue. You can do it on the component level by setting the inheritAttrs option to false.

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

    

This will disable the fall through attributes feature in the component, meaning any listener or attribute that is passed to the component will not be applied to the component’s root element unless it is explicitly defined in the component’s props or emits.

Routing Attributes with useAttrs

Now that we have disabled the fall through attributes on a component, we can manually route the attributes to the component’s root element using the useAttrs function.

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

const attrs = useAttrs();

// { class: 'mt-4', style: 'color: red;', onclick: [Function] }
console.log(attrs.value);
</script>

    

useAttrs returns an object containing all the attributes and listeners that are passed to the component. From there, you can choose to do whatever you want with them.

For example, maybe we want to apply the attributes to the second root element:

      <template>
  <div>First Element</div>
  <div v-bind="attrs">Second Element</div>
</template>

    

Or maybe we could apply the attributes to both elements!

      <template>
  <div v-bind="attrs">First Element</div>
  <div v-bind="attrs">Second Element</div>
</template>

    

We could also apply some attributes to one element and the rest to the other element:

      <template>
  <div v-bind="rootAttrs">
    <input type="text" v-bind="inputAttrs" />
  </div>
</template>

<script setup>
import { computed } from 'vue';
import { omit } from 'lodash-es';

const attrs = useAttrs();

const rootAttrs = computed(() => {
  return omit(attrs.value, ['class', 'style']);
});

const inputAttrs = computed(() => {
  return omit(attrs.value, ['class', 'style']);
});
</script>

    

That way we can apply placeholder and change to the input safely, and it would have the intended effect. I call this “attribute routing”.

The sky is the limit here. You could omit certain attributes before re-applying them to the root element. You are in full control over what happens.

Bonus: $attrs

Keep in mind that fall through attributes are available in the template through $attrs, so you don’t have to use useAttrs in the script to grab them. If it’s a quick routing, you can just use $attrs in the template.

      <template>
  <div>First Element</div>
  <div v-bind="$attrs">Second Element</div>
</template>

    

That way you can apply the attributes to the root element and the input element safely, and it would have the intended effect without having to use useAttrs in the script.

Another tip: you don’t have to disable the fall through attributes to be able to use $attrs or useAttrs as they will be populated with the non-explicitly defined attributes and listeners either way.

Conclusion

Fall through attributes are a powerful feature in Vue.js that can be used to style, listen for events, and match selectors on the component’s root element. However, they can also be a bit of a gotcha, and you should be aware of the scenarios where they can go wrong.

By disabling the fall through attributes feature and manually routing the attributes to their intended elements using useAttrs or $attrs, you can avoid most of the pitfalls and have full control over what happens to the attributes and listeners that are passed to the component.

Having said that, I don’t recommend overusing the fall through attributes behavior.

While it is great for flexibility and ease of use, I would still always recommend having a clear-cut API for your components. I usually like class and style attributes to fall through, maybe data-test-id for testing selectors, but that’s about it.

More certificates.dev articles

Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.

Looking for Certified Developers?

We can help you recruit Certified Developers for your organization or project. The team has helped many customers employ suitable resources from a pool of 100s of qualified Developers.

Let us help you get the resources you need.

Contact Us
Customer Testimonial for Hiring
like a breath of fresh air
Everett Owyoung
Everett Owyoung
Head of Talent for ThousandEyes
(a Cisco company)