Fallthrough attributes are props and event listeners passed to a component that aren't declared in defineProps or defineEmits. Vue automatically forwards them to the component's root element. This includes class, style, id, data-*, aria-*, and event listeners.
vue
<!-- Parent -->
<BaseButton class="primary" data-testid="submit" @click="save">
Save
</BaseButton>
<!-- BaseButton.vue (no class or click declared) -->
<template>
<button><slot /></button>
</template>
<!-- Rendered HTML: attrs fall through to the root <button> -->
<button class="primary" data-testid="submit">Save</button>Disabling automatic fallthrough
When a component has multiple root elements, or you need to apply attrs to a non-root element, disable inheritance and bind $attrs manually:
vue
<script setup>
defineOptions({ inheritAttrs: false })
</script>
<template>
<div class="wrapper">
<!-- Forward all attrs to the inner input, not the wrapper -->
<input v-bind="$attrs" />
<span class="icon">🔍</span>
</div>
</template>Accessing attrs in script
Use useAttrs() to read fallthrough attributes in <script setup>:
vue
<script setup>
import { useAttrs } from 'vue'
defineOptions({ inheritAttrs: false })
const attrs = useAttrs()
function handleClick(event: MouseEvent) {
console.log('internal logic first')
// Forward the original click listener
const onClick = attrs.onClick as ((e: MouseEvent) => void) | undefined
onClick?.(event)
}
</script>
<template>
<button @click="handleClick"><slot /></button>
</template>Attribute naming in $attrs
| Parent template | Key in $attrs |
|---|---|
class="foo" | attrs.class |
data-id="123" | attrs['data-id'] |
aria-label="..." | attrs['aria-label'] |
@click="fn" | attrs.onClick |
@custom-event="fn" | attrs.onCustomEvent |
Hyphenated attributes need bracket notation. Event listeners become camelCase onX keys.
useAttrs is not reactive
useAttrs() always returns the latest values, but watchers don't track it:
ts
const attrs = useAttrs()
// This watcher never fires on attr changes
watch(() => attrs.class, (val) => { /* dead code */ })
// Use onUpdated instead
onUpdated(() => {
console.log('current attrs:', attrs)
})If you need to react to a specific attribute changing, promote it to a prop with defineProps. Props are fully reactive.
When fallthrough attrs matter
| Scenario | What to do |
|---|---|
| Wrapper around a native element (input, button) | Let attrs fall through, or bind $attrs to the right element |
| Component with multiple roots | Set inheritAttrs: false, bind $attrs explicitly |
| Need to intercept an event before forwarding | Read attrs.onClick, call it after your logic |
| Need reactive access to a specific attr | Promote it to a declared prop |