Telemetry signals that are not defined as props automatically "pass through" to the component's root element - like data from sensors to the NOVA LAB central processor. This mechanism is called fallthrough attributes and is an important aspect of creating flexible components.
Attributes passed to a component that are NOT declared as props or emits automatically end up on the root element of the template:
1<!-- Parent -->
2<CustomInput class="special" data-testid="email" />
3
4<!-- CustomInput.vue (root = input) -->
5<template>
6 <input type="text" />
7</template>
8
9<!-- Result in DOM -->
10<input type="text" class="special" data-testid="email" />This applies to:
class, style, id, data-*, event listeners, and other HTML attributes. This behavior is very useful - it allows the component user to add CSS classes, inline styles, or accessibility attributes without needing to define them as props.If the root element of the component already has its own
class or style, Vue automatically merges them with attributes from the parent:1<!-- Parent -->
2<StatusPanel class="highlighted" style="margin-top: 10px" />
3
4<!-- StatusPanel.vue -->
5<template>
6 <div class="panel" style="padding: 1rem">...</div>
7</template>
8
9<!-- Result in DOM - classes and styles merged -->
10<div class="panel highlighted" style="padding: 1rem; margin-top: 10px">...</div>Event listeners also pass through. If the parent passes
@click, it will be added to the root element:1<!-- Parent -->
2<MarsButton @click="handleClick" />
3
4<!-- MarsButton.vue -->
5<template>
6 <button @click="logInternal">Click</button>
7</template>In this case, clicking the button will trigger both handlers - first
logInternal from the component, then handleClick from the parent.When you want to control exactly where attributes go (e.g., assign them to an inner element instead of root), use
inheritAttrs: false:1<script setup>
2defineOptions({ inheritAttrs: false })
3</script>
4
5<template>
6 <div class="wrapper">
7 <!-- Manual assignment to a specific element -->
8 <input v-bind="$attrs" type="text" />
9 </div>
10</template>This is a common pattern in form components - the wrapper
<div> serves for styling, while $attrs go directly to the <input>, where they're most useful (e.g., placeholder, disabled, aria-label).Access attributes in
<script setup> via the useAttrs() composable:1<script setup>
2import { useAttrs } from 'vue'
3
4const attrs = useAttrs()
5
6console.log(attrs.class) // "special"
7console.log(attrs['data-testid']) // "email"
8console.log(attrs.style) // style object
9</script>Note:
useAttrs() is NOT reactive - if you need to react to attribute changes, use $attrs directly in the template or the onUpdated hook.When a component has multiple root elements (fragments), Vue doesn't know which one to assign fallthrough attributes to. You must explicitly assign
$attrs to the chosen element:1<script setup>
2defineOptions({ inheritAttrs: false })
3</script>
4
5<template>
6 <header>Header</header>
7 <main v-bind="$attrs">Content</main>
8 <footer>Footer</footer>
9</template>Without
inheritAttrs: false and explicit v-bind="$attrs", Vue will display a warning in the console. This requires a conscious decision - which element should "inherit" attributes from the parent.Let's combine all concepts in one example:
1<script setup>
2import { useAttrs } from 'vue'
3
4defineOptions({ inheritAttrs: false })
5
6defineProps({ title: String })
7
8const attrs = useAttrs()
9const hasCustomClass = !!attrs.class
10</script>
11
12<template>
13 <section class="nova-panel">
14 <h3>{{ title }}</h3>
15 <div v-bind="$attrs" class="panel-content">
16 <slot />
17 </div>
18 <small v-if="hasCustomClass">Custom styling applied</small>
19 </section>
20</template>Thanks to this pattern, the parent can pass
class, style, id, and other attributes that go directly to panel-content, not to the outer <section>.