Skip to content

Commit d46b74f

Browse files
committed
feat(Alert): add new component
1 parent afac21e commit d46b74f

18 files changed

Lines changed: 1080 additions & 2 deletions

File tree

apps/docs/src/components/docs/DocsApi.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
if (!prefix) return []
4141
4242
return Object.entries(data.components)
43-
.filter(([name]) => name.startsWith(prefix))
43+
.filter(([name]) => name === prefix || name.startsWith(`${prefix}.`))
4444
.map(([, api]) => api)
4545
.toSorted((a, b) => {
4646
if (a.name.endsWith('Root')) return -1
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script setup lang="ts">
2+
import { Alert } from '@vuetify/v0'
3+
</script>
4+
5+
<template>
6+
<div class="flex flex-col gap-4 p-4">
7+
<Alert.Root
8+
id="info-alert"
9+
class="flex items-start gap-3 rounded-lg border border-info/30 bg-info/10 p-4 text-sm"
10+
>
11+
<Alert.Icon class="mt-0.5 shrink-0 text-info">
12+
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
13+
<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" />
14+
<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" />
15+
</svg>
16+
</Alert.Icon>
17+
18+
<div class="flex-1">
19+
<Alert.Title class="font-semibold text-on-surface">
20+
Scheduled maintenance
21+
</Alert.Title>
22+
23+
<Alert.Description class="mt-1 text-on-surface/70">
24+
The service will be unavailable on Sunday from 2–4 AM UTC.
25+
</Alert.Description>
26+
</div>
27+
</Alert.Root>
28+
29+
<Alert.Root
30+
id="warning-alert"
31+
class="flex items-start gap-3 rounded-lg border border-warning/30 bg-warning/10 p-4 text-sm"
32+
>
33+
<Alert.Icon class="mt-0.5 shrink-0 text-warning">
34+
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
35+
<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" />
36+
</svg>
37+
</Alert.Icon>
38+
39+
<div class="flex-1">
40+
<Alert.Title class="font-semibold text-on-surface">
41+
Low disk space
42+
</Alert.Title>
43+
44+
<Alert.Description class="mt-1 text-on-surface/70">
45+
You have less than 500 MB remaining. Consider freeing up space.
46+
</Alert.Description>
47+
</div>
48+
</Alert.Root>
49+
</div>
50+
</template>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<script setup lang="ts">
2+
import { Alert } from '@vuetify/v0'
3+
import { shallowRef } from 'vue'
4+
5+
const showInfo = shallowRef(true)
6+
const showError = shallowRef(true)
7+
</script>
8+
9+
<template>
10+
<div class="flex flex-col gap-4 p-4">
11+
<button
12+
v-if="!showInfo || !showError"
13+
class="self-start rounded border border-divider px-3 py-1.5 text-sm hover:bg-surface-tint"
14+
@click="showInfo = true; showError = true"
15+
>
16+
Reset
17+
</button>
18+
19+
<Alert.Root
20+
v-if="showInfo"
21+
v-model="showInfo"
22+
class="flex items-start gap-3 rounded-lg border border-success/30 bg-success/10 p-4 text-sm"
23+
>
24+
<Alert.Icon class="mt-0.5 shrink-0 text-success">
25+
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
26+
<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" />
27+
</svg>
28+
</Alert.Icon>
29+
30+
<div class="flex-1">
31+
<Alert.Title class="font-semibold text-on-surface">
32+
Deployment successful
33+
</Alert.Title>
34+
35+
<Alert.Description class="mt-1 text-on-surface/70">
36+
Version 2.4.1 is now live. No action required.
37+
</Alert.Description>
38+
</div>
39+
40+
<Alert.Action
41+
class="shrink-0 rounded p-1 text-on-surface/50 hover:bg-black/10 hover:text-on-surface"
42+
>
43+
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
44+
<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" />
45+
</svg>
46+
</Alert.Action>
47+
</Alert.Root>
48+
49+
<Alert.Root
50+
v-if="showError"
51+
v-model="showError"
52+
class="flex items-start gap-3 rounded-lg border border-error/30 bg-error/10 p-4 text-sm"
53+
>
54+
<Alert.Icon class="mt-0.5 shrink-0 text-error">
55+
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
56+
<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" />
57+
</svg>
58+
</Alert.Icon>
59+
60+
<div class="flex-1">
61+
<Alert.Title class="font-semibold text-on-surface">
62+
Payment failed
63+
</Alert.Title>
64+
65+
<Alert.Description class="mt-1 text-on-surface/70">
66+
We couldn't charge your card ending in 4242. Please update your payment method.
67+
</Alert.Description>
68+
</div>
69+
70+
<Alert.Action
71+
class="shrink-0 rounded p-1 text-on-surface/50 hover:bg-black/10 hover:text-on-surface"
72+
>
73+
<svg class="size-4" fill="currentColor" viewBox="0 0 16 16">
74+
<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" />
75+
</svg>
76+
</Alert.Action>
77+
</Alert.Root>
78+
</div>
79+
</template>

apps/docs/src/pages/components/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Components with meaningful HTML defaults. Render semantic elements by default bu
7575

7676
| Name | Description |
7777
| - | - |
78+
| [Alert](/components/semantic/alert) | Inline status banner with icon, title, description, and dismiss action |
7879
| [Avatar](/components/semantic/avatar) | Image/fallback avatar with priority loading |
7980
| [Breadcrumbs](/components/semantic/breadcrumbs) | Navigation breadcrumbs with overflow detection and truncation |
8081
| [Carousel](/components/semantic/carousel) | Scroll-snap slide navigation with multi-slide display and drag/swipe |
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
---
2+
title: Alert - Inline Status Banner Component
3+
meta:
4+
- name: description
5+
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.
6+
- name: keywords
7+
content: alert, banner, notification, status, warning, error, success, info, dismissible, Vue 3, headless, accessible, WAI-ARIA
8+
features:
9+
category: Component
10+
label: 'C: Alert'
11+
github: /components/Alert/
12+
renderless: false
13+
level: 2
14+
related:
15+
- /components/semantic/snackbar
16+
- /components/disclosure/alert-dialog
17+
- /components/semantic/progress
18+
---
19+
20+
# Alert
21+
22+
Inline status banner for persistent, non-interrupting feedback — errors, warnings, and contextual notices.
23+
24+
<DocsPageFeatures :frontmatter />
25+
26+
## Usage
27+
28+
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.
29+
30+
::: example
31+
/components/alert/basic
32+
33+
### Informational and warning banners
34+
35+
Static alerts with icon, title, and description — the most common pattern for system notices, in-form error summaries, and section-level warnings.
36+
37+
| Sub-component | Role |
38+
|---|---|
39+
| `Alert.Root` | Container; carries `role="alert"` and ARIA ID links |
40+
| `Alert.Icon` | Decorative icon wrapper; `aria-hidden="true"` by default |
41+
| `Alert.Title` | Heading with auto-generated ID for `aria-labelledby` |
42+
| `Alert.Description` | Body text with auto-generated ID for `aria-describedby` |
43+
44+
:::
45+
46+
## Anatomy
47+
48+
```vue Anatomy playground collapse
49+
<script setup lang="ts">
50+
import { Alert } from '@vuetify/v0'
51+
</script>
52+
53+
<template>
54+
<Alert.Root>
55+
<Alert.Icon />
56+
<Alert.Title />
57+
<Alert.Description />
58+
<Alert.Action />
59+
</Alert.Root>
60+
</template>
61+
```
62+
63+
## Examples
64+
65+
::: example
66+
/components/alert/dismissible
67+
68+
### Dismissible alerts
69+
70+
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`.
71+
72+
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.
73+
74+
```vue
75+
<template>
76+
<Alert.Root v-if="visible" v-model="visible">
77+
...
78+
<Alert.Action>✕</Alert.Action>
79+
</Alert.Root>
80+
</template>
81+
```
82+
83+
You can also react to the model externally — for example, to persist the dismissed state across sessions:
84+
85+
```ts
86+
const dismissed = useStorage('alert-dismissed', false)
87+
```
88+
89+
```vue
90+
<template>
91+
<Alert.Root v-if="!dismissed" v-model:model-value="isDismissed => dismissed = isDismissed">
92+
...
93+
</Alert.Root>
94+
</template>
95+
```
96+
97+
| File | Role |
98+
|---|---|
99+
| `dismissible.vue` | Two dismissible alerts with v-model binding and a reset button |
100+
101+
:::
102+
103+
## Alert vs Snackbar vs AlertDialog
104+
105+
| | Alert | Snackbar | AlertDialog |
106+
|---|---|---|---|
107+
| **Position** | Inline, in document flow | Floating overlay | Modal overlay |
108+
| **Auto-dismiss** | No | Yes (configurable) | No |
109+
| **Interrupts focus** | No | No | Yes — focus moves to dialog |
110+
| **ARIA** | `role="alert"` | `role="status"` / `role="alert"` | `role="alertdialog"` |
111+
| **Use case** | Persistent page-level notices | Transient confirmations | Requires explicit acknowledgement |
112+
113+
> [!TIP]
114+
> 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.
115+
116+
> [!WARNING]
117+
> 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.
118+
119+
## Accessibility
120+
121+
### ARIA roles and attributes
122+
123+
| Attribute | Element | Value | Purpose |
124+
|---|---|---|---|
125+
| `role` | `Alert.Root` | `"alert"` | Declares a live region with `aria-live="assertive"` implicit semantics |
126+
| `aria-labelledby` | `Alert.Root` | `{id}-title` | Links root to `Alert.Title` for accessible name |
127+
| `aria-describedby` | `Alert.Root` | `{id}-description` | Links root to `Alert.Description` for supplementary text |
128+
| `id` | `Alert.Title` | `{id}-title` | Target for `aria-labelledby` |
129+
| `id` | `Alert.Description` | `{id}-description` | Target for `aria-describedby` |
130+
| `aria-hidden` | `Alert.Icon` | `"true"` | Hides decorative icon from screen reader tree |
131+
| `type` | `Alert.Action` | `"button"` | Prevents accidental form submission |
132+
| `aria-label` | `Alert.Action` | locale `Alert.dismiss` | Accessible name for icon-only dismiss buttons |
133+
134+
### Screen reader behavior
135+
136+
`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.
137+
138+
For non-urgent status messages (e.g. "saved successfully"), prefer [Snackbar](/components/semantic/snackbar) which uses `aria-live="polite"` and does not interrupt.
139+
140+
### Icon accessibility
141+
142+
`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:
143+
144+
```vue
145+
<template>
146+
<!-- Icon carries unique meaning — expose it -->
147+
<Alert.Icon :aria-hidden="false" aria-label="Error">
148+
<ErrorIcon />
149+
</Alert.Icon>
150+
</template>
151+
```
152+
153+
<DocsApi />

apps/docs/src/typed-router.d.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,13 @@ declare module 'vue-router/auto-routes' {
272272
Record<never, never>,
273273
| never
274274
>,
275+
'/components/semantic/alert': RouteRecordInfo<
276+
'/components/semantic/alert',
277+
'/components/semantic/alert',
278+
Record<never, never>,
279+
Record<never, never>,
280+
| never
281+
>,
275282
'/components/semantic/avatar': RouteRecordInfo<
276283
'/components/semantic/avatar',
277284
'/components/semantic/avatar',
@@ -1265,6 +1272,12 @@ declare module 'vue-router/auto-routes' {
12651272
views:
12661273
| never
12671274
}
1275+
'src/pages/components/semantic/alert.md': {
1276+
routes:
1277+
| '/components/semantic/alert'
1278+
views:
1279+
| never
1280+
}
12681281
'src/pages/components/semantic/avatar.md': {
12691282
routes:
12701283
| '/components/semantic/avatar'

dev/src/components.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export {}
1111
/* prettier-ignore */
1212
declare module 'vue' {
1313
export interface GlobalComponents {
14+
AlertAction: typeof import('./../../packages/0/src/components/Alert/AlertAction.vue')['default']
15+
AlertDescription: typeof import('./../../packages/0/src/components/Alert/AlertDescription.vue')['default']
1416
AlertDialogAction: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogAction.vue')['default']
1517
AlertDialogActivator: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogActivator.vue')['default']
1618
AlertDialogCancel: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogCancel.vue')['default']
@@ -19,6 +21,9 @@ declare module 'vue' {
1921
AlertDialogDescription: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogDescription.vue')['default']
2022
AlertDialogRoot: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogRoot.vue')['default']
2123
AlertDialogTitle: typeof import('./../../packages/0/src/components/AlertDialog/AlertDialogTitle.vue')['default']
24+
AlertIcon: typeof import('./../../packages/0/src/components/Alert/AlertIcon.vue')['default']
25+
AlertRoot: typeof import('./../../packages/0/src/components/Alert/AlertRoot.vue')['default']
26+
AlertTitle: typeof import('./../../packages/0/src/components/Alert/AlertTitle.vue')['default']
2227
AspectRatio: typeof import('./../../packages/0/src/components/AspectRatio/AspectRatio.vue')['default']
2328
Atom: typeof import('./../../packages/0/src/components/Atom/Atom.vue')['default']
2429
AvatarFallback: typeof import('./../../packages/0/src/components/Avatar/AvatarFallback.vue')['default']
@@ -95,6 +100,9 @@ declare module 'vue' {
95100
NumberFieldIncrement: typeof import('./../../packages/0/src/components/NumberField/NumberFieldIncrement.vue')['default']
96101
NumberFieldRoot: typeof import('./../../packages/0/src/components/NumberField/NumberFieldRoot.vue')['default']
97102
NumberFieldScrub: typeof import('./../../packages/0/src/components/NumberField/NumberFieldScrub.vue')['default']
103+
OverflowIndicator: typeof import('./../../packages/0/src/components/Overflow/OverflowIndicator.vue')['default']
104+
OverflowItem: typeof import('./../../packages/0/src/components/Overflow/OverflowItem.vue')['default']
105+
OverflowRoot: typeof import('./../../packages/0/src/components/Overflow/OverflowRoot.vue')['default']
98106
PaginationEllipsis: typeof import('./../../packages/0/src/components/Pagination/PaginationEllipsis.vue')['default']
99107
PaginationFirst: typeof import('./../../packages/0/src/components/Pagination/PaginationFirst.vue')['default']
100108
PaginationItem: typeof import('./../../packages/0/src/components/Pagination/PaginationItem.vue')['default']

dev/src/plugins/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Framework
2+
import { Vuetify0DateAdapter } from '@vuetify/v0/date'
3+
14
// Types
25
import type { App } from 'vue'
36

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

1720
app.use(createLoggerPlugin())
1821

22+
app.use(createDatePlugin({
23+
adapter: new Vuetify0DateAdapter(),
24+
locales: { en: 'en-US' },
25+
}))
26+
1927
app.use(
2028
createBreakpointsPlugin({
2129
//

packages/0/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ import { ... } from '@vuetify/v0/date' // Date adapter and utilities
136136
137137
| Component | Description |
138138
|-----------|-------------|
139+
| [Alert](https://0.vuetifyjs.com/components/semantic/alert) | Inline status banner with icon, title, description, and dismiss action |
139140
| [Avatar](https://0.vuetifyjs.com/components/semantic/avatar) | Image/fallback avatar with priority loading |
140141
| [Breadcrumbs](https://0.vuetifyjs.com/components/semantic/breadcrumbs) | Navigation breadcrumbs with overflow detection and truncation |
141142
| [Carousel](https://0.vuetifyjs.com/components/semantic/carousel) | Scroll-snap slide navigation with multi-slide display and drag/swipe |

0 commit comments

Comments
 (0)