Skip to main content
Vuetify0 v1.0 is here
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

BuTabs

Edit this page
Report a Bug
Open issues
Copy Markdown

Renders elementIntermediateAug 24, 2026

Bulma’s .tabs with selection and panels — the JavaScript and the tabpanels the CSS framework never documented.

Note

Reference: Tabs on bulma.io↗︎ — classes and visual variants. This page is the JavaScript.

Usage

BuTabs is the selection context and renders no element of its own. Modifiers live on BuTabList. BuTab siblings go inside the list; BuTabPanel siblings of the list hold the content. v-model is the selected tab’s value; leave it empty and the first tab is selected.

<script setup lang="ts">
  import { BuTab, BuTabList, BuTabPanel, BuTabs } from '@paper/bulma'

  import { shallowRef } from 'vue'

  const tab = shallowRef('pictures')
</script>

<template>
  <BuTabs v-model="tab">
    <BuTabList>
      <BuTab value="pictures">Pictures</BuTab>

      <BuTab value="music">Music</BuTab>
    </BuTabList>

    <BuTabPanel value="pictures">
      <div class="content mt-4">
        <p>Pictures panel.</p>
      </div>
    </BuTabPanel>

    <BuTabPanel value="music">
      <div class="content mt-4">
        <p>Music panel.</p>
      </div>
    </BuTabPanel>
  </BuTabs>
</template>

Anatomy

vue
<script setup lang="ts">
  import { BuTab, BuTabList, BuTabPanel, BuTabs } from '@paper/bulma'
</script>

<template>
  <BuTabs>
    <BuTabList>
      <BuTab />
    </BuTabList>

    <BuTabPanel />
  </BuTabs>
</template>

Composed on v0

BuTabs maps onto v0’s Tabs compound: Tabs.Root, Tabs.List, Tabs.Item and Tabs.Panel.

BuTabs is Tabs.Root with no element of its own. Bulma’s .tabs block is the list, and the panels are siblings of that block, so the selection context has to wrap both. Every .tabs modifier — centered, boxed, toggle, size — therefore lives on BuTabList, which is the div.tabs plus the ul (Tabs.List as="ul"). Putting those classes on the root would paint a wrapper Bulma does not document.

BuTab is Tabs.Item with as set to null, not the renderless prop. as feeds the item’s button polyfill, so renderless alone would leak type="button" and disabled onto the anchor. The item renders li.is-active > a itself, which is what the fixture demands: is-active on the li, no href on the a.

That renderless item registers no element. v0’s focusSelectedTab / focusAdjacent then become no-ops — Arrow keys still move selection, panels and aria-selected, but DOM focus stays on the previously selected anchor. That is a documented APG deviation, not an oversight; the v0-core follow-up is to let TabsItem accept an element via registration so a renderless consumer can supply the focus target.

BuTabPanel is Tabs.Panel as-is. The hidden attribute is fine here: Bulma defines no panel CSS, so there is no class for the wrapper to fight.

The markup you know

The Bulma tab is the markup published on bulma.io↗︎, captured verbatim in the conformance fixture. The Vue tab is the component that renders it. Upstream documents no tabpanels — those are component territory. The conformance suite diffs the list.

html
<div class="tabs">
  <ul>
    <li class="is-active"><a>Pictures</a></li>
    <li><a>Music</a></li>
    <li><a>Videos</a></li>
    <li><a>Documents</a></li>
  </ul>
</div>

You write no is-active. The first tab is selected when v-model is empty; after that, the model owns the class.

Examples

Boxed

boxed is is-boxed on .tabs — the bordered, folder-tab look. It is a list modifier, so it lives on BuTabList with centered, toggle and size, not on BuTabs. The root has no class to hang a modifier on.

Reach for boxed when the tabs sit on a content panel and you want the selected tab to read as attached to it. toggle is the other shape: mutually exclusive buttons, no folder metaphor. The two modifiers are independent classes in Bulma, so stacking them is possible and usually a mistake — pick the look the surrounding layout is built for.

Size still belongs on the list. small / medium / large scale the tabs without changing which item is selected, and they compose with boxed the same way they compose with the default underline style.

Props

BuTabs is the selection context. Everything visible is a part.

PropTypeDefaultDescription
v-modelTSelected tab value. Empty selects the first tab
PartRendersProps
BuTabListdiv.tabs + ulcentered, boxed, toggle, size ('small' | 'normal' | 'medium' | 'large')
BuTabli > avalue, disabled
BuTabPaneldiv[role=tabpanel]value (required)

BuTab and BuTabPanel both expose isSelected as a slot prop.

Accessibility

BuTabList’s ul is role="tablist". Each BuTab puts role="presentation" on the li so the list’s required children are the role="tab" anchors, not the list items — without that, axe flags aria-required-children / aria-required-parent. Selected state is aria-selected on the tab and hidden on the matching role="tabpanel", which is labelled through aria-labelledby.

KeyBehavior
ClickSelects that tab
Arrow Left / RightMoves selection, not focus
Home / EndSelects first / last
Warning

Arrow keys update selection, panels and aria-selected, but the focus ring and the screen-reader cursor stay on the tab that was focused. BuTab is renderless, so v0 has no element to move focus to. This is an APG tabs deviation the axe sweep cannot catch. Click still focuses the tab you clicked; keyboard users who start on a tab and arrow away will hear the old tab while a new panel is showing.

Was this page helpful?

© 2016-1970 Vuetify, LLC
Services
Ctrl+/