BuTabs
Bulma’s .tabs with selection and panels — the JavaScript and the tabpanels the CSS framework never documented.
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.
Anatomy
<script setup lang="ts">
import { BuTab, BuTabList, BuTabPanel, BuTabs } from '@paper/bulma'
</script>
<template>
<BuTabs>
<BuTabList>
<BuTab />
</BuTabList>
<BuTabPanel />
</BuTabs>
</template>Composed on Vuetify0
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. The <a> is passed as el so arrow-key focus follows the real control.
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.
<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><template>
<BuTabs>
<BuTabList>
<BuTab value="pictures">Pictures</BuTab>
<BuTab value="music">Music</BuTab>
<BuTab value="videos">Videos</BuTab>
<BuTab value="documents">Documents</BuTab>
</BuTabList>
</BuTabs>
</template>You write no is-active. The first tab is selected when v-model is empty; after that, the model owns the class.
Examples
Props
BuTabs is the selection context. Everything visible is a part.
| Prop | Type | Default | Description |
|---|---|---|---|
v-model | T | — | Selected tab value. Empty selects the first tab |
| Part | Renders | Props |
|---|---|---|
BuTabList | div.tabs + ul | centered, boxed, toggle, size ('small' | 'normal' | 'medium' | 'large') |
BuTab | li > a | value, disabled |
BuTabPanel | div[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.
| Key | Behavior |
|---|---|
| Click | Selects that tab |
| Arrow Left / Right | Moves focus and activates the tab |
| Home / End | Focuses and activates first / last |