Use it for
Use BbButton for actions the user performs — save, delete, submit — and for
destinations that need button styling. Actions use @click or type="submit";
navigation uses href or to.
Use something else when
BbDropdownButton— one dominant action has close alternatives: Save / Save as draftBbDropdown— no action dominatesBbBaseButton— the surface is clickable with no button chrome at all: a table row, a card
Pass Through
Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.
Default
BbButton is the library's button, and its label is slot content. type
defaults to button, so inside a form the only button that submits is the one
you mark type="submit".
<template>
<div class="flex flex-wrap items-center gap-3">
<BbButton>Save changes</BbButton>
<BbButton variant="secondary">Preview</BbButton>
<BbButton variant="outline">Cancel</BbButton>
</div>
</template>
<script setup lang="ts">
import { BbButton } from 'bitboss-ui';
</script>
The variant ladder
Variants are a hierarchy, not a palette: one primary per region of the page,
then secondary, outline, ghost.
Keep destructive for what cannot be undone. link recolors the button without
changing its box, so it is the wrong choice for an inline text link.
<template>
<div class="flex flex-wrap items-center gap-2">
<BbButton variant="primary">Save</BbButton>
<BbButton variant="secondary">Duplicate</BbButton>
<BbButton variant="outline">Export</BbButton>
<BbButton variant="ghost">Cancel</BbButton>
<BbButton variant="destructive">Delete</BbButton>
</div>
</template>
<script setup lang="ts">
import { BbButton } from 'bitboss-ui';
</script>
Sizes and width
Six sizes, xs through 2xl, derived from the global --bb-control-h scale, so
height, padding, text and icons move together. md is the default and lines up
with a compact field; block stretches the button to its container.
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap items-center gap-2">
<BbButton size="xs" variant="outline">xs</BbButton>
<BbButton size="sm" variant="outline">sm</BbButton>
<BbButton size="md" variant="outline">md</BbButton>
<BbButton size="lg" variant="outline">lg</BbButton>
<BbButton size="xl" variant="outline">xl</BbButton>
<BbButton size="2xl" variant="outline">2xl</BbButton>
</div>
<div class="max-w-xs">
<BbButton variant="primary" block>Continue</BbButton>
</div>
</div>
</template>
<script setup lang="ts">
import { BbButton } from 'bitboss-ui';
</script>
Icons
prepend:icon and append:icon put an icon beside the label. For an icon-only
button use icon and keep writing the label in the slot — it is hidden visually
and still announced. Use aria-label only when that accessible name must differ
from the slot text; never ship an icon-only button with neither.
<template>
<div class="flex flex-wrap items-center gap-2">
<BbButton variant="primary" prepend:icon="lucide:plus">New invoice</BbButton>
<BbButton variant="outline" append:icon="lucide:download">Export</BbButton>
<!-- Icon-only: the label is still slot content, rendered sr-only. -->
<BbButton variant="ghost" icon="lucide:settings">Settings</BbButton>
</div>
</template>
<script setup lang="ts">
import { BbButton } from 'bitboss-ui';
</script>
Async actions
A click handler that returns a promise puts the button into its loading state
until the promise settles and blocks repeat clicks. Fire-and-forget work is not
trackable; return the promise or bind loading to the state that owns it.
<template>
<div class="flex flex-wrap items-center gap-3">
<BbButton variant="primary" @click="onSave">Save changes</BbButton>
<span class="text-sm opacity-70">{{ status }}</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbButton } from 'bitboss-ui';
import { delay } from '~/demo-data';
const status = ref('No changes saved yet.');
/**
* The handler is async and nothing sets a loading flag: the button tracks the
* returned promise itself and stays busy until it settles.
*/
const onSave = async () => {
status.value = 'Saving…';
await delay(null, 1200);
status.value = 'Changes saved.';
};
</script>
Coming from v2
In v2 this was opt-in through auto-loading. Delete that prop in v3. Use
disable-auto-loading only when you intentionally need the old behaviour.
A submit button has no async @click handler to track. Bind loading to form or
request state instead; BbForm exposes
isSubmitting for returned submit work.
Buttons that navigate
Pass href or to instead of navigating from a click handler. The result is a
real link with middle-click, open-in-new-tab and copy-address behaviour.
<template>
<div class="flex flex-wrap items-center gap-2">
<!-- Renders an <a>: the browser gets a real link, with middle-click,
open-in-new-tab and copy-address all working. -->
<BbButton variant="outline" href="https://www.npmjs.com/package/bitboss-ui" target="_blank">
View on npm
</BbButton>
<BbButton variant="link" href="#toggle">Jump to toggles</BbButton>
</div>
</template>
<script setup lang="ts">
import { BbButton } from 'bitboss-ui';
</script>
Toggles and exclusive sets
v-model operates the button as a toggle between true-value and false-value.
Seed the model — an undefined value swallows the first click.
For an exclusive set, share one model and give each button its own true-value,
with false-value pointing at that same value so the selection is mandatory.
<template>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3">
<BbButton v-model="notifications" variant="outline" prepend:icon="lucide:bell">
Notifications
</BbButton>
<span class="text-sm opacity-70">{{ notifications ? 'On' : 'Off' }}</span>
</div>
<!--
An exclusive set: one shared model, and every button's false-value
mirrors its OWN true-value, so re-clicking the active one keeps it
selected instead of switching away.
-->
<div class="flex items-center gap-1">
<BbButton
v-for="option in views"
:key="option.value"
v-model="view"
group
variant="outline"
:true-value="option.value"
:false-value="option.value"
:prepend:icon="option.icon"
>
{{ option.label }}
</BbButton>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbButton } from 'bitboss-ui';
// Seed the model: an undefined model is not a toggle yet, so the first click
// would do nothing.
const notifications = ref(false);
const view = ref('board');
const views = [
{ value: 'board', label: 'Board', icon: 'lucide:columns-3' },
{ value: 'table', label: 'Table', icon: 'lucide:table' },
];
</script>
Custom variants
Register a name in the plugin config and it becomes a typed value of variant:
// nuxt.config.ts
export default defineNuxtConfig({
bitboss: { buttonVariants: ['upgrade'] },
});
The library ships no CSS for registered variants — you style
.bb-button--upgrade yourself through the same local properties as the built-in
variants.
Use BbBaseButton instead when the control
needs a completely custom shape such as a card, row or inline link. A custom
variant is still a button; the base component is the primitive.
.bb-button--upgrade {
--bg: var(--bb-primary);
--bg-hover: color-mix(in oklab, var(--bb-primary) 90%, black);
--bg-pressed: color-mix(in oklab, var(--bb-primary) 80%, black);
--fg: var(--bb-primary-fg);
--ring: var(--bb-ring);
}
Coming from v2v2 tooltip and theme props
Replace tooltip* props with v-bb-tooltip or BbTooltip. Replace theme
with a built-in or registered variant; unregistered variant names are now
type errors.