Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/docs/src/components/docs/DocsApi.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
if (!prefix) return []

return Object.entries(data.components)
.filter(([name]) => name.startsWith(prefix))
.filter(([name]) => name === prefix || name.startsWith(`${prefix}.`))
.map(([, api]) => api)
.toSorted((a, b) => {
if (a.name.endsWith('Root')) return -1
Expand Down
50 changes: 50 additions & 0 deletions apps/docs/src/examples/components/alert/basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { Alert } from '@vuetify/v0'
</script>

<template>
<div class="flex flex-col gap-4 p-4">
<Alert.Root
id="info-alert"
class="flex items-start gap-3 rounded-lg border border-info/30 bg-info/10 p-4 text-sm"
>
<Alert.Icon class="mt-0.5 shrink-0 text-info">
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
<path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" />
</svg>
</Alert.Icon>

<div class="flex-1">
<Alert.Title class="font-semibold text-on-surface">
Scheduled maintenance
</Alert.Title>

<Alert.Description class="mt-1 text-on-surface/70">
The service will be unavailable on Sunday from 2–4 AM UTC.
</Alert.Description>
</div>
</Alert.Root>

<Alert.Root
id="warning-alert"
class="flex items-start gap-3 rounded-lg border border-warning/30 bg-warning/10 p-4 text-sm"
>
<Alert.Icon class="mt-0.5 shrink-0 text-warning">
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
<path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</svg>
</Alert.Icon>

<div class="flex-1">
<Alert.Title class="font-semibold text-on-surface">
Low disk space
</Alert.Title>

<Alert.Description class="mt-1 text-on-surface/70">
You have less than 500 MB remaining. Consider freeing up space.
</Alert.Description>
</div>
</Alert.Root>
</div>
</template>
79 changes: 79 additions & 0 deletions apps/docs/src/examples/components/alert/dismissible.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<script setup lang="ts">
import { Alert } from '@vuetify/v0'
import { shallowRef } from 'vue'

const showInfo = shallowRef(true)
const showError = shallowRef(true)
</script>

<template>
<div class="flex flex-col gap-4 p-4">
<button
v-if="!showInfo || !showError"
class="self-start rounded border border-divider px-3 py-1.5 text-sm hover:bg-surface-tint"
@click="showInfo = true; showError = true"
>
Reset
</button>

<Alert.Root
v-if="showInfo"
v-model="showInfo"
class="flex items-start gap-3 rounded-lg border border-success/30 bg-success/10 p-4 text-sm"
>
<Alert.Icon class="mt-0.5 shrink-0 text-success">
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" />
</svg>
</Alert.Icon>

<div class="flex-1">
<Alert.Title class="font-semibold text-on-surface">
Deployment successful
</Alert.Title>

<Alert.Description class="mt-1 text-on-surface/70">
Version 2.4.1 is now live. No action required.
</Alert.Description>
</div>

<Alert.Action
class="shrink-0 rounded p-1 text-on-surface/50 hover:bg-black/10 hover:text-on-surface"
>
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854z" />
</svg>
</Alert.Action>
</Alert.Root>

<Alert.Root
v-if="showError"
v-model="showError"
class="flex items-start gap-3 rounded-lg border border-error/30 bg-error/10 p-4 text-sm"
>
<Alert.Icon class="mt-0.5 shrink-0 text-error">
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zM5.354 4.646a.5.5 0 1 0-.708.708L7.293 8l-2.647 2.646a.5.5 0 0 0 .708.708L8 8.707l2.646 2.647a.5.5 0 0 0 .708-.708L8.707 8l2.647-2.646a.5.5 0 0 0-.708-.708L8 7.293 5.354 4.646z" />
</svg>
</Alert.Icon>

<div class="flex-1">
<Alert.Title class="font-semibold text-on-surface">
Payment failed
</Alert.Title>

<Alert.Description class="mt-1 text-on-surface/70">
We couldn't charge your card ending in 4242. Please update your payment method.
</Alert.Description>
</div>

<Alert.Action
class="shrink-0 rounded p-1 text-on-surface/50 hover:bg-black/10 hover:text-on-surface"
>
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854z" />
</svg>
</Alert.Action>
</Alert.Root>
</div>
</template>
1 change: 1 addition & 0 deletions apps/docs/src/pages/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Components with meaningful HTML defaults. Render semantic elements by default bu

| Name | Description |
| - | - |
| [Alert](/components/semantic/alert) | Inline status banner with icon, title, description, and dismiss action |
| [Avatar](/components/semantic/avatar) | Image/fallback avatar with priority loading |
| [Breadcrumbs](/components/semantic/breadcrumbs) | Navigation breadcrumbs with overflow detection and truncation |
| [Carousel](/components/semantic/carousel) | Scroll-snap slide navigation with multi-slide display and drag/swipe |
Expand Down
153 changes: 153 additions & 0 deletions apps/docs/src/pages/components/semantic/alert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
---
title: Alert - Inline Status Banner Component
meta:
- name: description
content: Headless inline alert component with role="alert" for status messages, warnings, and dismissible banners. Composable sub-components for icon, title, description, and dismiss action.
- name: keywords
content: alert, banner, notification, status, warning, error, success, info, dismissible, Vue 3, headless, accessible, WAI-ARIA
features:
category: Component
label: 'C: Alert'
github: /components/Alert/
renderless: false
level: 2
related:
- /components/semantic/snackbar
- /components/disclosure/alert-dialog
- /components/semantic/progress
---

# Alert

Inline status banner for persistent, non-interrupting feedback — errors, warnings, and contextual notices.

<DocsPageFeatures :frontmatter />

## Usage

Alert renders with `role="alert"` so screen readers announce the content automatically when the component enters the DOM. Use it for feedback that belongs inline with page content and does not require user acknowledgement.

::: example
/components/alert/basic

### Informational and warning banners

Static alerts with icon, title, and description — the most common pattern for system notices, in-form error summaries, and section-level warnings.

| Sub-component | Role |
|---|---|
| `Alert.Root` | Container; carries `role="alert"` and ARIA ID links |
| `Alert.Icon` | Decorative icon wrapper; `aria-hidden="true"` by default |
| `Alert.Title` | Heading with auto-generated ID for `aria-labelledby` |
| `Alert.Description` | Body text with auto-generated ID for `aria-describedby` |

:::

## Anatomy

```vue Anatomy playground collapse
<script setup lang="ts">
import { Alert } from '@vuetify/v0'
</script>

<template>
<Alert.Root>
<Alert.Icon />
<Alert.Title />
<Alert.Description />
<Alert.Action />
</Alert.Root>
</template>
```

## Examples

::: example
/components/alert/dismissible

### Dismissible alerts

Alerts can be made dismissible by adding `Alert.Action` and binding `v-model` on `Alert.Root`. When the action is clicked, `dismiss()` is called internally and `v-model` is set to `false`.

Use `v-if` on `Alert.Root` to remove the element from the DOM after dismissal — `role="alert"` elements should not stay in the DOM silently, because some screen readers re-announce live regions when page state changes.

```vue
<template>
<Alert.Root v-if="visible" v-model="visible">
...
<Alert.Action>✕</Alert.Action>
</Alert.Root>
</template>
```

You can also react to the model externally — for example, to persist the dismissed state across sessions:

```ts
const dismissed = useStorage('alert-dismissed', false)
```

```vue
<template>
<Alert.Root v-if="!dismissed" v-model:model-value="isDismissed => dismissed = isDismissed">
...
</Alert.Root>
</template>
```

| File | Role |
|---|---|
| `dismissible.vue` | Two dismissible alerts with v-model binding and a reset button |

:::

## Alert vs Snackbar vs AlertDialog

| | Alert | Snackbar | AlertDialog |
|---|---|---|---|
| **Position** | Inline, in document flow | Floating overlay | Modal overlay |
| **Auto-dismiss** | No | Yes (configurable) | No |
| **Interrupts focus** | No | No | Yes — focus moves to dialog |
| **ARIA** | `role="alert"` | `role="status"` / `role="alert"` | `role="alertdialog"` |
| **Use case** | Persistent page-level notices | Transient confirmations | Requires explicit acknowledgement |

> [!TIP]
> Alerts present in the DOM **before** page load are not announced by most screen readers — the live region only fires on mutation. Dynamically insert the alert (via `v-if`) after user action or data load to guarantee announcement.

> [!WARNING]
> Do not auto-dismiss alerts. The WAI-ARIA spec notes that alerts that disappear automatically violate WCAG 2.0 criterion 2.2.3 (No Timing). If you need ephemeral, auto-dismissing feedback, use [Snackbar](/components/semantic/snackbar) instead.

## Accessibility

### ARIA roles and attributes

| Attribute | Element | Value | Purpose |
|---|---|---|---|
| `role` | `Alert.Root` | `"alert"` | Declares a live region with `aria-live="assertive"` implicit semantics |
| `aria-labelledby` | `Alert.Root` | `{id}-title` | Links root to `Alert.Title` for accessible name |
| `aria-describedby` | `Alert.Root` | `{id}-description` | Links root to `Alert.Description` for supplementary text |
| `id` | `Alert.Title` | `{id}-title` | Target for `aria-labelledby` |
| `id` | `Alert.Description` | `{id}-description` | Target for `aria-describedby` |
| `aria-hidden` | `Alert.Icon` | `"true"` | Hides decorative icon from screen reader tree |
| `type` | `Alert.Action` | `"button"` | Prevents accidental form submission |
| `aria-label` | `Alert.Action` | locale `Alert.dismiss` | Accessible name for icon-only dismiss buttons |

### Screen reader behavior

`role="alert"` has implicit `aria-live="assertive"` and `aria-atomic="true"` in all major browsers. When an alert enters or changes in the DOM, the entire alert content is announced immediately, interrupting any in-progress announcements.

For non-urgent status messages (e.g. "saved successfully"), prefer [Snackbar](/components/semantic/snackbar) which uses `aria-live="polite"` and does not interrupt.

### Icon accessibility

`Alert.Icon` renders `aria-hidden="true"` by default. If your icon communicates meaning beyond what the text already conveys (e.g. an icon that is the *only* indicator of severity), remove `aria-hidden` and provide a visible or screen-reader-only label instead:

```vue
<template>
<!-- Icon carries unique meaning — expose it -->
<Alert.Icon :aria-hidden="false" aria-label="Error">
<ErrorIcon />
</Alert.Icon>
</template>
```

<DocsApi />
13 changes: 13 additions & 0 deletions apps/docs/src/typed-router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'/components/semantic/alert': RouteRecordInfo<
'/components/semantic/alert',
'/components/semantic/alert',
Record<never, never>,
Record<never, never>,
| never
>,
'/components/semantic/avatar': RouteRecordInfo<
'/components/semantic/avatar',
'/components/semantic/avatar',
Expand Down Expand Up @@ -1265,6 +1272,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/pages/components/semantic/alert.md': {
routes:
| '/components/semantic/alert'
views:
| never
}
'src/pages/components/semantic/avatar.md': {
routes:
| '/components/semantic/avatar'
Expand Down
8 changes: 8 additions & 0 deletions dev/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AlertAction: typeof import('./../../packages/0/src/components/Alert/AlertAction.vue')['default']
AlertDescription: typeof import('./../../packages/0/src/components/Alert/AlertDescription.vue')['default']
AlertDialogAction: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogAction.vue')['default']
AlertDialogActivator: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogActivator.vue')['default']
AlertDialogCancel: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogCancel.vue')['default']
Expand All @@ -19,6 +21,9 @@ declare module 'vue' {
AlertDialogDescription: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogDescription.vue')['default']
AlertDialogRoot: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogRoot.vue')['default']
AlertDialogTitle: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogTitle.vue')['default']
AlertIcon: typeof import('./../../packages/0/src/components/Alert/AlertIcon.vue')['default']
AlertRoot: typeof import('./../../packages/0/src/components/Alert/AlertRoot.vue')['default']
AlertTitle: typeof import('./../../packages/0/src/components/Alert/AlertTitle.vue')['default']
AspectRatio: typeof import('./../../packages/0/src/components/AspectRatio/AspectRatio.vue')['default']
Atom: typeof import('./../../packages/0/src/components/Atom/Atom.vue')['default']
AvatarFallback: typeof import('./../../packages/0/src/components/Avatar/AvatarFallback.vue')['default']
Expand Down Expand Up @@ -95,6 +100,9 @@ declare module 'vue' {
NumberFieldIncrement: typeof import('./../../packages/0/src/components/NumberField/NumberFieldIncrement.vue')['default']
NumberFieldRoot: typeof import('./../../packages/0/src/components/NumberField/NumberFieldRoot.vue')['default']
NumberFieldScrub: typeof import('./../../packages/0/src/components/NumberField/NumberFieldScrub.vue')['default']
OverflowIndicator: typeof import('./../../packages/0/src/components/Overflow/OverflowIndicator.vue')['default']
OverflowItem: typeof import('./../../packages/0/src/components/Overflow/OverflowItem.vue')['default']
OverflowRoot: typeof import('./../../packages/0/src/components/Overflow/OverflowRoot.vue')['default']
PaginationEllipsis: typeof import('./../../packages/0/src/components/Pagination/PaginationEllipsis.vue')['default']
PaginationFirst: typeof import('./../../packages/0/src/components/Pagination/PaginationFirst.vue')['default']
PaginationItem: typeof import('./../../packages/0/src/components/Pagination/PaginationItem.vue')['default']
Expand Down
8 changes: 8 additions & 0 deletions dev/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Framework
import { Vuetify0DateAdapter } from '@vuetify/v0/date'

// Types
import type { App } from 'vue'

Expand All @@ -16,6 +19,11 @@ export function registerPlugins (app: App) {

app.use(createLoggerPlugin())

app.use(createDatePlugin({
adapter: new Vuetify0DateAdapter(),
locales: { en: 'en-US' },
}))

app.use(
createBreakpointsPlugin({
//
Expand Down
1 change: 1 addition & 0 deletions packages/0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ import { ... } from '@vuetify/v0/date' // Date adapter and utilities

| Component | Description |
|-----------|-------------|
| [Alert](https://0.vuetifyjs.com/components/semantic/alert) | Inline status banner with icon, title, description, and dismiss action |
| [Avatar](https://0.vuetifyjs.com/components/semantic/avatar) | Image/fallback avatar with priority loading |
| [Breadcrumbs](https://0.vuetifyjs.com/components/semantic/breadcrumbs) | Navigation breadcrumbs with overflow detection and truncation |
| [Carousel](https://0.vuetifyjs.com/components/semantic/carousel) | Scroll-snap slide navigation with multi-slide display and drag/swipe |
Expand Down
Loading
Loading