Skip to content

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

Open

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.

Activity

  1. BatLeDev commented on Aug 10, 2026

    @BatLeDev
    ContributorAuthor

    Extra data point: this is a regression introduced in 3.0.0. In Vuetify 2 the same pattern produced valid HTML.

    VBtn content VBadge root VBadge wrapper badge inside button
    2.6.14 span.v-btn__content span span valid
    3.0.0 → 4.1.8 span.v-btn__content props.tag, default div div (hardcoded) invalid

    In v2.6.14 VBadge.ts the root was a span:

    return h('span', { staticClass: 'v-badge', attrs, class: this.classes }, children)

    and the wrapper too:

    genBadgeWrapper (): VNode {
      return this.$createElement('span', { staticClass: 'v-badge__wrapper' }, [this.genBadge()])
    }

    VBtn has wrapped its default slot in span.v-btn__content in both major versions (v2.6.14 VBtn.ts), so the nesting only became invalid when the 3.0.0 rewrite switched both badge elements to div. It has been that way for the whole v3 and v4 line.

    That also means restoring span for both is a return to the v2 behaviour rather than a new decision.

  2. BatLeDev commented on Aug 10, 2026

    @BatLeDev
    ContributorAuthor

    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-items and 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 > div where button > span was expected) reported for VDatePicker. 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.

  3. added
    T: bugFunctionality that does not work as intended/expected
    P: lowThe issue is of low importance
    on Aug 11, 2026
  4. waterWang commented on Aug 11, 2026

    @waterWang
    Contributor

    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 the tag prop so that when tag="span" is passed, both the root and wrapper elements produce valid phrasing content inside VBtns span.v-btn__content`.

  5. added
    audit toolsa problem affecting auditing tools with no effect on the actual behavior
    on Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    C: VBadgeP: lowThe issue is of low importanceT: bugFunctionality that does not work as intended/expectedaudit toolsa problem affecting auditing tools with no effect on the actual behavior

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions