Chapter 58: Vue $attrs Object
The $attrs object (also written as v-bind=”$attrs” or simply “the attrs object”)
This is not the same as props — it’s the second half of the story when a parent passes attributes to a child component.
Many developers spend months using Vue without truly understanding $attrs, and then one day they discover it and think: “Wow… I could have saved so many lines of code and made my components so much cleaner!”
What exactly is $attrs? (Very clear mental model)
When a parent component uses your component like this:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<MyButton class="large primary" type="submit" disabled @click="handleClick" data-testid="submit-btn" aria-label="Submit form" custom-prop="hello" > Save </MyButton> |
Vue does the following automatic splitting:
- Anything that matches a declared prop → goes into props
- Everything else → goes into $attrs
So in the child component (MyButton.vue):
- props gets: nothing (in this example — unless you declared custom-prop)
- $attrs gets all of these:
- class: “large primary”
- type: “submit”
- disabled: true
- @click listener
- data-testid
- aria-label
- custom-prop: “hello”
$attrs is basically a catch-all bag containing:
- All non-prop attributes (class, style, id, data-, aria-, etc.)
- All event listeners (@click, @focus, @keydown.enter, custom events…)
- All unknown props (things parent passed that child didn’t declare)
Most Common & Most Valuable Use-Case (2026)
Forwarding / passing through attributes to the root element
Almost every reusable UI component (Button, Input, Card, Modal, etc.) should do this.
Before understanding $attrs (bad/old style — lots of manual work):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!-- Bad: you have to list every possible attribute --> <button :class="[$attrs.class, 'base-btn']" :style="$attrs.style" :id="$attrs.id" :disabled="$attrs.disabled" v-bind="$attrs" <!-- wait... but this includes @click twice! --> > <slot /> </button> |
Modern, clean way (what you should always do in 2026)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<!-- src/components/AppButton.vue --> <template> <!-- v-bind="$attrs" forwards EVERYTHING automatically --> <button v-bind="$attrs" class="app-btn"> <slot /> </button> </template> <script setup lang="ts"> // Optional: disable automatic inheritance if you want full control defineOptions({ inheritAttrs: false // ← very common in UI libraries }) // Now $attrs is just a normal object — you can spread it wherever you want </script> <style scoped> .app-btn { padding: 0.8rem 1.6rem; background: #3b82f6; color: white; border: none; border-radius: 6px; font-weight: 500; cursor: pointer; } </style> |
Parent usage — looks completely normal
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<AppButton class="large primary" type="submit" disabled @click="handleSubmit" data-testid="submit-btn" aria-label="Submit form" > Save Changes </AppButton> |
Rendered DOM (what browser sees)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<button class="app-btn large primary" <!-- merged --> type="submit" disabled data-testid="submit-btn" aria-label="Submit form" @click="handleSubmit" <!-- listener forwarded --> > Save Changes </button> |
→ Zero manual listing — everything is automatically passed through
Important Option: inheritAttrs: false (Very Common in 2026)
By default Vue automatically applies $attrs to the root element of the component.
If you don’t want that (very common in UI libraries), you turn it off:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<script setup> defineOptions({ inheritAttrs: false // ← disables auto-forwarding }) </script> <template> <div class="wrapper"> <!-- Now YOU decide where to put the attributes --> <button v-bind="$attrs" class="my-btn"> <slot /> </button> </div> </template> |
→ $attrs becomes a normal object you can spread anywhere — on button, div, span, custom component, etc.
What Exactly Is Inside $attrs? (Full Breakdown)
| Type of attribute passed by parent | Goes into $attrs? | Example | Notes |
|---|---|---|---|
| Normal HTML attributes | Yes | id, class, style, tabindex, data-*, aria-* | Automatically merged for class/style |
| Event listeners | Yes | @click, @focus, @keydown.enter, @custom-event | Forwarded as functions |
| Props that child did NOT declare | Yes | unknown-prop=”value” | Treated as attrs |
| Props that child did declare | No | title=”Hello” (if defineProps([‘title’])) | Goes to props instead |
| v-model sugar | Yes (as modelValue + listener) | v-model=”value” | Becomes modelValue + onUpdate:modelValue |
Quick Summary Table – $attrs vs props
| Question | $attrs | props |
|---|---|---|
| What does it contain? | Non-prop attributes + event listeners | Only declared props |
| Auto-applied to root element? | Yes (unless inheritAttrs: false) | No — you use them manually |
| Can child modify them? | Yes (but usually you forward them) | No — props are read-only |
| Used for forwarding? | Yes — v-bind=”$attrs” | No |
| Contains event listeners? | Yes | No |
| TypeScript type | Record<string, any> | What you declare in defineProps |
Pro Tips from Real Projects (Hyderabad 2026)
- UI library / design system components → almost always use inheritAttrs: false + v-bind=”$attrs” on the root interactive element (button, input, div…)
- Class & style merging is intelligent — Vue combines parent’s classes with child’s classes
- Event listeners are forwarded correctly — parent can listen to @click even if child has its own @click
- $attrs.class and $attrs.style are objects → safe to spread/merge
- Accessibility attributes (aria-*, role, tabindex) → always forward them via $attrs
- Custom data attributes (data-testid, data-cy) → automatically forwarded — great for testing
Your Mini Practice Task
- Create AppButton.vue with inheritAttrs: false
- Put v-bind=”$attrs” on the real <button>
- Use it from parent with: class, type, disabled, @click, data-testid, aria-label
- Check DevTools → confirm everything landed on the <button> except declared props
Any part confusing? Want full examples for:
- $attrs + TypeScript typing?
- Forwarding $attrs to multiple child elements?
- $attrs vs $props vs $emit comparison?
- Real UI library button/input with full attribute forwarding?
Just tell me — we’ll build the next clean, forwarded component together 🚀
Happy attr-forwarding from Hyderabad! 💙
