Skip to content

[Bug Report][4.1.8] VBadge renders divs inside VBtn's span, producing invalid HTML #23093

Description

@BatLeDev

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:

  1. render v-badge__wrapper as a span instead of a div;
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C: VBadgeP: lowThe issue is of low importanceT: bugFunctionality that does not work as intended/expected

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions