Repository navigation
[Bug Report][4.1.8] VBadge renders divs inside VBtn's span, producing invalid HTML #23093
Description
Environment
Vuetify Version: 4.1.8
Vue Version: 3.5.38
Browsers: Chrome
OS: Linux
Steps to reproduce
Put a v-badge inside a v-btn — the pattern shown in the VBadge documentation — and run the rendered page through the W3C validator (https://validator.w3.org/nu):
<v-toolbar>
<v-toolbar-items>
<v-btn title="Notifications" stacked>
<v-badge content="3" color="warning">
<v-icon icon="mdi-bell" />
</v-badge>
</v-btn>
</v-toolbar-items>
</v-toolbar>Expected Behavior
Valid HTML. VBtn wraps its default slot in <span class="v-btn__content">, which is phrasing content, so what is rendered inside it should be phrasing content too.
Actual Behavior
VBadge renders a div root plus a hardcoded div.v-badge__wrapper:
<span class="v-btn__content">
<div class="v-badge">
<div class="v-badge__wrapper">
<i class="mdi-bell v-icon"></i>
<span class="v-badge__badge bg-warning">3</span>
</div>
</div>
</span>The W3C validator reports:
Element “div” not allowed as child of element “span” in this context. (Suppressing further errors from this subtree.)
The suppression matters in practice: the validator stops checking that subtree, so any other error below it goes unreported until this one is fixed.
The public tag prop only changes the outer element. v-badge__wrapper is hardcoded in VBadge and has no API, so tag="span" alone is not enough to make the output valid.
Reproduction Link
https://play.vuetifyjs.com/playgrounds/798dTg
The playground asserts the nesting at runtime and prints the rendered button markup.
Other comments
The usual workaround does not apply here. The natural answer is "wrap the button in the badge instead", but our use case is a notification button inset in a v-toolbar, and inverting the nesting breaks the inset styling in three ways:
.v-toolbar-items > .v-btn { border-radius: 0 }no longer matches, becausediv.v-badgebecomes the toolbar-items child — the button gets its rounded corners back.- The button no longer stretches to the toolbar height: its parent goes from
.v-toolbar-items(height: inherit; align-self: stretch) to.v-badge__wrapper, whose height is auto, so it collapses to content height. .v-badge__badgeis positioned against.v-badge__wrapper, which goes from hugging the icon to wrapping the whole button — the counter jumps to the button's top-right corner and can be clipped by the toolbar.
So for a toolbar-inset button, badge-inside-button is the only nesting that renders correctly, and it is the one that produces invalid HTML.
Suggested fix — in packages/vuetify/src/components/VBadge/VBadge.tsx:
- render
v-badge__wrapperas aspaninstead of adiv; - optionally default
makeTagProps({ tag: 'span' }), so the documented badge-on-a-button pattern is valid without the consumer having to passtag="span".
This cannot change rendering: Vuetify already declares the display of both elements explicitly — .v-badge { display: inline-block } and .v-badge__wrapper { display: flex } — so the tag name carries no layout meaning. I checked the same markup with span in both places against the W3C validator: 0 errors, identical rendering.
Happy to open a PR if that would help.
Activity
BatLeDev commentedon Aug 10, 2026
Extra data point: this is a regression introduced in 3.0.0. In Vuetify 2 the same pattern produced valid HTML.
VBtncontentVBadgerootVBadgewrapperspan.v-btn__contentspanspanspan.v-btn__contentprops.tag, defaultdivdiv(hardcoded)In v2.6.14
VBadge.tsthe root was aspan:and the wrapper too:
VBtnhas wrapped its default slot inspan.v-btn__contentin both major versions (v2.6.14VBtn.ts), so the nesting only became invalid when the 3.0.0 rewrite switched both badge elements todiv. It has been that way for the whole v3 and v4 line.That also means restoring
spanfor both is a return to the v2 behaviour rather than a new decision.BatLeDev commentedon Aug 10, 2026
Related prior reports, for context — neither covers this defect, but one is directly relevant:
[Bug Report][3.3.19] VBadge Buttons do not render correctly when in VToolbarItems #18390 — "VBadge Buttons do not render correctly when in VToolbarItems" (3.3.19, closed as not planned / stale). That report is the mirror image of this one: it uses the inverse nesting (badge around the button) inside
v-toolbar-itemsand reports that the button then fails to render like the other toolbar buttons. It is a visual-rendering report, not a validity one, so it does not overlap — but it independently confirms that the natural workaround for the invalid nesting is itself broken in a toolbar. With badge-inside-button producing invalid HTML and badge-around-button producing broken layout, there is currently no correct option for a badged toolbar button.[Bug Report] DIV inside BUTTON does not validate #13572 — "DIV inside BUTTON does not validate" (2.4.9), the same class of defect (
button > divwherebutton > spanwas expected) reported forVDatePicker. Different component, but it shows this kind of markup-validity fix has been treated as a real bug before.[Enhancements] Make sure Vuetify generates valid HTML #2301 — an old general "make sure Vuetify generates valid HTML" enhancement, closed long ago.
waterWang commentedon Aug 11, 2026
I will work on this fix. The issue is that VBadge hardcodes
<div class="v-badge__wrapper">inside its template — the fix is to make the wrapper element use thetagprop so that whentag="span"is passed, both the root and wrapper elements produce valid phrasing content inside VBtnsspan.v-btn__content`.