Alert
Show guidance, warnings, operation results, and recoverable failures in an Alert. Add your own headings, text, links, and buttons, then choose whether the message needs an announcement.
Klean does not infer urgency from color or from the component name. Alert renders no role and no live region by default.
<script setup>
import Alert from '@/components/ui/alert/Alert.vue'
</script>
<template>
<Alert
as="section"
role="alert"
aria-labelledby="deployment-error-title"
class="bg-red-50 text-red-950 dark:bg-red-950 dark:text-red-100"
>
<h2 id="deployment-error-title" class="font-medium">Deployment failed</h2>
<p class="mt-1 leading-6 text-red-800 dark:text-red-200">
The server could not be reached. No production files changed.
</p>
<button type="button" class="mt-4 cursor-pointer font-medium underline">
Try again
</button>
</Alert>
</template>import Alert from '@/components/ui/alert/Alert.jsx'
export default function DeploymentError() {
return (
<Alert
as="section"
role="alert"
aria-labelledby="deployment-error-title"
className="bg-red-50 text-red-950 dark:bg-red-950 dark:text-red-100"
>
<h2 id="deployment-error-title" className="font-medium">
Deployment failed
</h2>
<p className="mt-1 leading-6 text-red-800 dark:text-red-200">
The server could not be reached. No production files changed.
</p>
<button
type="button"
className="mt-4 cursor-pointer font-medium underline"
>
Try again
</button>
</Alert>
)
}<script>
import Alert from '$lib/components/ui/alert/Alert.svelte'
</script>
<Alert
as="section"
role="alert"
aria-labelledby="deployment-error-title"
class="bg-red-50 text-red-950 dark:bg-red-950 dark:text-red-100"
>
<h2 id="deployment-error-title" class="font-medium">Deployment failed</h2>
<p class="mt-1 leading-6 text-red-800 dark:text-red-200">
The server could not be reached. No production files changed.
</p>
<button type="button" class="mt-4 cursor-pointer font-medium underline">
Try again
</button>
</Alert>View source Alert.vue
<script setup>
import { computed, ref, useAttrs } from 'vue'
import { twMerge } from 'tailwind-merge'
defineOptions({ inheritAttrs: false })
defineProps({
as: { type: [String, Object, Function], default: 'div' }
})
const attrs = useAttrs()
const element = ref()
const forwardedAttrs = computed(() => {
const { class: _class, 'data-slot': _dataSlot, ...rest } = attrs
return rest
})
defineExpose({ element })
</script>
<template>
<component
:is="as"
ref="element"
v-bind="forwardedAttrs"
data-slot="alert"
:class="
twMerge(
'relative w-full rounded-md bg-gray-100 p-4 text-sm text-gray-950 dark:bg-gray-900 dark:text-white',
attrs.class
)
"
>
<slot />
</component>
</template>Installation
One command detects Vue, React, or Svelte and writes the matching source into the application's conventional component directory:
Run from your application’s directory. Klean detects your framework and copies the matching source.
npx klean-ui add alertUsage
The application writes the real content and opts into urgent announcement only when a new failure needs immediate attention.
Copy the example above for your framework.
API
| Input | Default | Purpose |
|---|---|---|
as | div | Native container such as div, section, or aside. |
class / className | — | Ordinary Tailwind classes merged after the neutral defaults. |
| native attributes | — | IDs, role, ARIA relationships, test hooks, and other native attributes. |
| default content | — | Native headings, paragraphs, lists, links, buttons, and application UI. |
Choose semantics from the lifecycle
Appearance cannot decide how assistive technology should announce a message.
| Situation | Markup |
|---|---|
| Static guidance already present with the page | No role |
| Supporting information that is useful but not a live update | role="note" when the extra semantic is worthwhile |
| A non-urgent result inserted after an operation | role="status" or an existing polite live region |
| An urgent failure inserted after the user acts | role="alert" |
Do not put role="alert" on every red surface or on every individual field error. A form may announce one error summary while each field keeps its own visible error and aria-describedby relationship.
Compose real checklists
A warnings checklist is still an Alert. The heading, count, items, suggestions, and actions remain native markup so application data and operations stay obvious.
View source DeploymentChecklist.vue
<script setup>
import Alert from '@/components/ui/alert/Alert.vue'
import Button from '@/components/ui/button/Button.vue'
const items = [
{
key: 'secret',
label: 'SESSION_SECRET is missing',
suggestion: 'Generate a strong secret before the first deployment.',
action: 'Generate'
},
{
key: 'database',
label: 'PostgreSQL is not attached',
suggestion: 'Add a database service for production data.',
action: 'Add service'
}
]
function handleAction(item) {
// The application owns the actual operation and its pending/error state.
console.log(item.key)
}
</script>
<template>
<Alert
as="section"
role="note"
aria-labelledby="deployment-checklist-title"
class="overflow-hidden border border-amber-200 bg-amber-50/50 p-0 text-gray-950 dark:border-amber-900/50 dark:bg-amber-950/20 dark:text-white"
>
<header class="px-4 py-3">
<h2
id="deployment-checklist-title"
class="text-sm font-medium text-amber-900 dark:text-amber-200"
>
Deployment checklist
</h2>
</header>
<ul class="border-t border-amber-200/60 dark:border-amber-900/40">
<li
v-for="item in items"
:key="item.key"
class="flex items-start justify-between gap-3 border-b border-amber-200/50 px-4 py-3 last:border-b-0 dark:border-amber-900/30"
>
<div class="min-w-0">
<p class="text-sm">{{ item.label }}</p>
<p class="mt-0.5 text-xs leading-5 text-gray-600 dark:text-gray-400">
{{ item.suggestion }}
</p>
</div>
<Button
type="button"
class="min-h-8 min-w-0 shrink-0 border border-amber-300 bg-transparent px-2.5 py-1 text-xs text-amber-900 hover:bg-amber-100 dark:border-amber-800 dark:bg-transparent dark:text-amber-200"
@click="handleAction(item)"
>
{{ item.action }}
</Button>
</li>
</ul>
</Alert>
</template>
The list preserves item count and navigation for screen-reader users. Visible words—not colored dots—carry each result. Buttons keep native keyboard behavior, while the application owns pending state, success, failure, and the actual operation.
Style products, not variants
Use Tailwind to make an alert expressive or compact while keeping its content and announcement behavior appropriate to the message.
View source AlertRecipes.vue
<script setup>
import Alert from '@/components/ui/alert/Alert.vue'
</script>
<template>
<Alert
as="section"
role="alert"
aria-labelledby="deployment-error-title"
class="bg-red-50 text-red-950 dark:bg-red-950 dark:text-red-100"
>
<h2 id="deployment-error-title" class="font-medium">Deployment failed</h2>
<p class="mt-1 leading-6 text-red-800 dark:text-red-200">
The server could not be reached. No production files changed.
</p>
<button type="button" class="mt-4 cursor-pointer font-medium underline">
Try again
</button>
</Alert>
</template>
If a treatment repeats, make a small application-owned wrapper or recipe. Do not move product colors, spacing, icons, or severity mapping into Alert.
Accessibility
- Use a heading level that fits the page outline; Alert never chooses one.
- Keep icons decorative with
aria-hidden="true"when visible text already communicates the meaning. - Do not communicate warning, success, or urgency through color alone.
- Use real buttons for actions and real anchors or Boring Stack Links for navigation.
- Keep dismiss controls explicitly labeled and application-owned.
- Ensure urgent messages are inserted after the live region exists; a server-rendered alert already present at page load may not be announced.
- Prefer one form error summary instead of many simultaneous assertive announcements.
Durable behavior
Alert owns no open, dismissed, loading, or severity state. Static guidance simply renders. Operation results derive from application state. If dismissal must survive navigation or sessions, the application chooses the correct Durable UI home and keeps a recovery path where the information matters.
Buttons inside an Alert own no hidden work. Pending state, cancellation, rollback, and error recovery remain with the operation that produced the message.
When to use
Use Alert for visible information that deserves a distinct surface: a warning, form summary, operation result, recovery instruction, compatibility note, or short checklist of blockers.
When not to use
- Use Toast for transient notification queues that should not occupy document layout.
- Use ordinary text when spacing and typography already make the message clear.
- Use Dialog when the user must make a focused modal decision.
- Use field errors beside Input and Textarea, with one optional summary for the form.
Complete framework source
Copy, inspect, and change the complete source for your framework.
<script setup>
import { computed, ref, useAttrs } from 'vue'
import { twMerge } from 'tailwind-merge'
defineOptions({ inheritAttrs: false })
defineProps({
as: { type: [String, Object, Function], default: 'div' }
})
const attrs = useAttrs()
const element = ref()
const forwardedAttrs = computed(() => {
const { class: _class, 'data-slot': _dataSlot, ...rest } = attrs
return rest
})
defineExpose({ element })
</script>
<template>
<component
:is="as"
ref="element"
v-bind="forwardedAttrs"
data-slot="alert"
:class="
twMerge(
'relative w-full rounded-md bg-gray-100 p-4 text-sm text-gray-950 dark:bg-gray-900 dark:text-white',
attrs.class
)
"
>
<slot />
</component>
</template>import { forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'
const BASE_CLASSES =
'relative w-full rounded-md bg-gray-100 p-4 text-sm text-gray-950 dark:bg-gray-900 dark:text-white'
const Alert = forwardRef(function Alert(
{ as: Component = 'div', className, 'data-slot': _dataSlot, ...props },
ref
) {
return (
<Component
{...props}
ref={ref}
data-slot="alert"
className={twMerge(BASE_CLASSES, className)}
/>
)
})
export default Alert<script>
import { twMerge } from "tailwind-merge";
let {
as = "div",
children,
class: className,
"data-slot": _dataSlot,
...props
} = $props();
let element;
export function getElement() {
return element;
}
</script>
<svelte:element
this={as}
{...props}
bind:this={element}
data-slot="alert"
class={twMerge(
"relative w-full rounded-md bg-gray-100 p-4 text-sm text-gray-950 dark:bg-gray-900 dark:text-white",
className,
)}
>
{#if children}
{@render children()}
{/if}
</svelte:element>