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, because div.v-badge becomes 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__badge is 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__wrapper as a span instead of a div;
- optionally default
makeTagProps({ tag: 'span' }), so the documented badge-on-a-button pattern is valid without the consumer having to pass tag="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.
Environment
Vuetify Version: 4.1.8
Vue Version: 3.5.38
Browsers: Chrome
OS: Linux
Steps to reproduce
Put a
v-badgeinside av-btn— the pattern shown in the VBadge documentation — and run the rendered page through the W3C validator (https://validator.w3.org/nu):Expected Behavior
Valid HTML.
VBtnwraps 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
VBadgerenders adivroot plus a hardcodeddiv.v-badge__wrapper:The W3C validator reports:
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
tagprop only changes the outer element.v-badge__wrapperis hardcoded inVBadgeand has no API, sotag="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..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:v-badge__wrapperas aspaninstead of adiv;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 withspanin both places against the W3C validator: 0 errors, identical rendering.Happy to open a PR if that would help.