We use cookies to enhance your experience on the site
CodeWorlds

Fallthrough Attributes

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.

Automatic Inheritance

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.

Merging classes and styles

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 listener inheritance

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.

Disabling Inheritance

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
).

useAttrs()

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.

Multi-root Components

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.

Practical example - NOVA LAB panel component

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>
.

Go to CodeWorlds