Use it for
Reach for BbAlert when the message stays on the page: errors above a form, a
quota warning, a "saved" confirmation in the panel that produced it.
Also see: Design language.
Passthrough
Hover or tap a part to outline it. The toggles turn on loading, errors and warnings where the component has them; only parts get a row.
Communicate a page state
Stack page-level messages at the top of the page they belong to, most urgent first.
Billing
- Plan
- Team, billed monthly
- Card
- Visa ending 4242
<template>
<section class="flex max-w-lg flex-col gap-3">
<h3 class="text-base font-semibold">Billing</h3>
<!-- Most urgent first. The two problems stay until they are fixed
(hide-close); only the product note can be dismissed. -->
<BbAlert
hide-close
icon="lucide:credit-card"
title="Your last payment failed"
variant="destructive"
>
The card ending 4242 was declined on 1 September.
<a class="underline" href="#billing-card">Update card</a>
</BbAlert>
<!-- One assertive alert at a time: the quota is urgent-looking but
can wait, so it announces politely. -->
<BbAlert
aria-live="polite"
hide-close
icon="lucide:hard-drive"
role="status"
:text="`${usedGb} of ${maxGb} GB used. Uploads stop when storage is full.`"
title="Storage almost full"
variant="warning"
/>
<!-- A neutral product note: outline plus an icon, the quietest block. -->
<BbAlert
v-model="showNote"
icon="lucide:languages"
text="Invoices can now be issued in Italian, German and French."
title="Localized invoices"
variant="outline"
/>
<dl
id="billing-card"
class="grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 border-t border-[color:var(--bb-border)] pt-3 text-sm"
>
<dt class="text-[color:var(--bb-text-muted)]">Plan</dt>
<dd>Team, billed monthly</dd>
<dt class="text-[color:var(--bb-text-muted)]">Card</dt>
<dd>Visa ending 4242</dd>
</dl>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbAlert } from 'bitboss-ui';
const usedGb = 46;
const maxGb = 50;
const showNote = ref(true);
</script>
Use hide-close when the message
clears only after the underlying state is fixed. The shortest form puts the body
in the default slot, <BbAlert>Something went wrong</BbAlert>. When a string is
all you have, pass it as text.
Variants
Pick the variant by intent, not decoration:
outlinefor neutral informationprimaryfor a branded note that asks for action, such as an upgradewarningfor something the user should attend todestructivefor a failure
<template>
<div class="flex max-w-md flex-col gap-3">
<BbAlert
hide-close
icon="lucide:info"
text="The catalogue shows net prices; VAT is added at checkout."
title="Prices exclude VAT"
/>
<BbAlert
hide-close
icon="lucide:bell-ring"
text="Stock now refreshes every 15 minutes, soon enough to warn you before a product sells out."
title="Turn on low-stock emails"
variant="primary"
/>
<BbAlert
hide-close
icon="lucide:triangle-alert"
:text="`${keyboard.name} is down to ${keyboard.stock} units.`"
title="Low stock"
variant="warning"
/>
<BbAlert
hide-close
icon="lucide:circle-x"
text="Row 42 has no SKU, so nothing was imported."
title="Import failed"
variant="destructive"
/>
</div>
</template>
<script setup lang="ts">
import { BbAlert } from 'bitboss-ui';
import { productById } from '~/demo-data';
const keyboard = productById[113]!;
</script>
outline is the default. No variant brings an icon of its own, so pair the
variant with an
icon, and pair them once: the object
form of alertVariants sets each variant's icon for the whole app
(Custom variants).
There is no success or info alert. A confirmation is usually a
toast, a neutral note is outline with an icon, and
anything else you register yourself.
Coming from v2theme → variant
In v2 theme took any string and painted nothing: v2 shipped no
.bb-alert--* rules at all. In v3 the string is live, and theme="warning"
becomes an amber alert, quietly. Grep your stylesheets for
.bb-alert--<value>: if nothing styled it, drop the prop.
Validation summaries
A destructive alert above the form, appearing on the first failed submit and
listing what is missing. Give it hide-close: the user clears this message by
fixing the fields.
<template>
<!--
The fields join the form by `name` and check themselves through `rules`.
The summary reads the same `errors` map from the slot: it appears after
the first failed submit and empties as each field is fixed.
-->
<BbForm
v-slot="{ errors, submitCount }"
class="flex max-w-md flex-col gap-3"
@submit="send"
>
<BbAlert
v-if="submitCount > 0 && Object.keys(errors).length > 0"
hide-close
icon="lucide:circle-x"
title="The invitation was not sent"
variant="destructive"
>
<ul class="m-0 list-inside list-disc p-0">
<li v-for="(messages, path) in errors" :key="path">
{{ messages[0] }}
</li>
</ul>
</BbAlert>
<BbTextInput
id="invite-name"
label="Full name"
name="name"
:rules="fullName"
/>
<BbTextInput
id="invite-email"
label="Work email"
name="email"
:rules="workEmail"
type="email"
/>
<div class="flex flex-wrap items-center gap-3">
<BbButton type="submit" variant="primary">
Send invitation
</BbButton>
<p class="m-0 text-sm text-(--bb-text-muted)" role="status">
{{ sentTo && `Invitation sent to ${sentTo}.` }}
</p>
</div>
</BbForm>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbAlert, BbButton, BbForm, BbTextInput } from 'bitboss-ui';
import { delay } from '~/demo-data';
const sentTo = ref('');
const fullName = (value: string | null) =>
Boolean(value?.trim()) || 'Enter the full name of the person to invite';
const workEmail = (value: string | null) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value ?? '') ||
'Enter a work email address, like [email protected]';
async function send(values: Record<string, unknown>) {
await delay(null, 600);
sentTo.value = String(values.email);
}
</script>
Inside a BbForm, read errors and submitCount from
its slot. The summary lists the messages each field already shows under itself,
and a failed submit still moves focus to the first invalid field. Keep it to one
summary rather than an alert per field.
Dismissal and v-model
The close button renders by default and works without a
v-model. For a message nobody
should be able to close, use hide-close.
Name the button for the message with
close-label when "Close" alone
would be vague.
<template>
<div class="flex max-w-md flex-col gap-3">
<!--
No v-if here: the component owns its own visibility and animates
itself out when the model turns false.
-->
<BbAlert
v-model="reminderVisible"
close-label="Dismiss the email verification reminder"
icon="lucide:mail"
:text="`We sent a link to ${owner.email}. It expires in 24 hours.`"
title="Verify your email"
variant="primary"
/>
<BbButton
v-if="!reminderVisible"
class="self-start"
prepend:icon="lucide:undo-2"
variant="ghost"
@click="reminderVisible = true"
>
Bring the reminder back
</BbButton>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbAlert, BbButton } from 'bitboss-ui';
import { userById } from '~/demo-data';
const owner = userById[1]!;
// Bound, so the page can read the state back and restore it. Left unbound the
// close button would still work — it would just be a one-way trip.
const reminderVisible = ref(true);
</script>
Bind v-model to observe the state and to put the alert back: setting it to
true replays the same transition inward. The alert owns its own visibility, so
it needs no wrapping v-if.
Coming from v2show-close → hide-close
The polarity flipped: :show-close="false" becomes hide-close, and a
show-close left at its default is deleted. In development the library
warns when it still finds a v2 name on the component.
Rich content
The #title and
#text slots replace the heading and the body. Both receive
{ text }, so you can decorate the string you were handed instead of restating
it.
<template>
<div class="max-w-md">
<BbAlert
hide-close
icon="lucide:sparkles"
text="Vantera Free keeps three active projects."
variant="primary"
>
<template #title> You are on the <strong>Free</strong> plan </template>
<!--
The slot receives the `text` prop, so the string stays where it is
and the slot only decorates it.
-->
<template #text="{ text }">
{{ text }}
<a class="font-medium underline" href="#pricing">Compare plans</a>
</template>
</BbAlert>
</div>
</template>
<script setup lang="ts">
import { BbAlert } from 'bitboss-ui';
</script>
Three sources can supply the body, and the most specific wins: #text, then
child content, then the text prop. There is no actions slot: a real anchor in
the text is fine, anything more interactive belongs outside the alert.
Coming from v2
The #text slot's scope used to hand you the title by mistake. It now carries
the text, so any workaround that read the prop from outside the slot can go.
How an alert is announced
warning and destructive announce as role="alert" and
aria-live="assertive". Every other variant uses role="status" and
aria-live="polite".
Force either one with the role prop when visual tone and urgency come
apart. The role brings its politeness along (alert is assertive, status
polite); aria-live is for the rare case that needs the two to differ.
<BbAlert
role="alert"
icon="lucide:cloud-off"
title="Autosave failed"
text="Your last edits exist only on this device."
/>
Never let several assertive alerts appear at once: they cut each other off.
Coming from v2
The alert is always aria-atomic, so a changed list re-announces whole. The v2
prop that turned that off is gone.
Custom variants
Register a name in the plugin's alertVariants and it becomes a typed value of
variant.
// nuxt.config.ts
export default defineNuxtConfig({
bitboss: { alertVariants: ['success'] },
});
The object form registers the names and sets each variant's icon for the whole app; a built-in name only gets its icon:
// nuxt.config.ts
export default defineNuxtConfig({
bitboss: {
alertVariants: {
warning: { icon: 'lucide:triangle-alert' },
destructive: { icon: 'lucide:circle-x' },
success: { icon: 'lucide:circle-check' }, // a project variant
},
},
});
An alert without its own icon shows its variant's, its own icon wins, and
hide-icon hides either:
<BbAlert variant="warning" title="Heads up" />
<BbAlert variant="warning" icon="lucide:clock" title="Runs at midnight" />
<BbAlert variant="warning" hide-icon title="Plain" />
Registered variants ship no CSS. You paint .bb-alert--<name> yourself with the
locals the built-ins set: --fg for the title and the icon (the body and the
close button follow it at 75%), --bg and --border-color for the fill and the
border. Mix
from a theme token such as --bb-success, which stays legible in dark mode.
.bb-alert--success {
--fg: color-mix(in oklab, var(--bb-success) 55%, var(--bb-text));
--border-color: color-mix(
in oklab,
var(--bb-success) 35%,
var(--bb-panel)
);
}
A registered variant is polite however alarming its name. Pass role="alert"
on the instances that genuinely are urgent.