diff --git a/apps/docs/src/components/docs/DocsApi.vue b/apps/docs/src/components/docs/DocsApi.vue
index 6ff35dbfa..d59fffe0b 100644
--- a/apps/docs/src/components/docs/DocsApi.vue
+++ b/apps/docs/src/components/docs/DocsApi.vue
@@ -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
diff --git a/apps/docs/src/examples/components/alert/basic.vue b/apps/docs/src/examples/components/alert/basic.vue
new file mode 100644
index 000000000..aacc6a162
--- /dev/null
+++ b/apps/docs/src/examples/components/alert/basic.vue
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+ Scheduled maintenance
+
+
+
+ The service will be unavailable on Sunday from 2–4 AM UTC.
+
+
+
+
+
+
+
+
+
+
+
+ Low disk space
+
+
+
+ You have less than 500 MB remaining. Consider freeing up space.
+
+
+
+
+
diff --git a/apps/docs/src/examples/components/alert/dismissible.vue b/apps/docs/src/examples/components/alert/dismissible.vue
new file mode 100644
index 000000000..fd9fab596
--- /dev/null
+++ b/apps/docs/src/examples/components/alert/dismissible.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Deployment successful
+
+
+
+ Version 2.4.1 is now live. No action required.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Payment failed
+
+
+
+ We couldn't charge your card ending in 4242. Please update your payment method.
+
+
+
+
+
+
+
+
+
diff --git a/apps/docs/src/pages/components/index.md b/apps/docs/src/pages/components/index.md
index 4f7239007..b824281d5 100644
--- a/apps/docs/src/pages/components/index.md
+++ b/apps/docs/src/pages/components/index.md
@@ -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 |
diff --git a/apps/docs/src/pages/components/semantic/alert.md b/apps/docs/src/pages/components/semantic/alert.md
new file mode 100644
index 000000000..95fbba543
--- /dev/null
+++ b/apps/docs/src/pages/components/semantic/alert.md
@@ -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.
+
+
+
+## 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
+
+
+
+
+
+
+
+
+
+
+```
+
+## 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
+
+
+ ...
+ ✕
+
+
+```
+
+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
+
+
+ ...
+
+
+```
+
+| 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
+
+
+
+
+
+
+```
+
+
diff --git a/apps/docs/src/typed-router.d.ts b/apps/docs/src/typed-router.d.ts
index b25560952..99ba978aa 100644
--- a/apps/docs/src/typed-router.d.ts
+++ b/apps/docs/src/typed-router.d.ts
@@ -272,6 +272,13 @@ declare module 'vue-router/auto-routes' {
Record,
| never
>,
+ '/components/semantic/alert': RouteRecordInfo<
+ '/components/semantic/alert',
+ '/components/semantic/alert',
+ Record,
+ Record,
+ | never
+ >,
'/components/semantic/avatar': RouteRecordInfo<
'/components/semantic/avatar',
'/components/semantic/avatar',
@@ -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'
diff --git a/dev/src/components.d.ts b/dev/src/components.d.ts
index 5b3969f93..9720b79bb 100644
--- a/dev/src/components.d.ts
+++ b/dev/src/components.d.ts
@@ -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']
@@ -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']
@@ -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']
diff --git a/dev/src/plugins/index.ts b/dev/src/plugins/index.ts
index bdc2bd67d..6b2da61f9 100644
--- a/dev/src/plugins/index.ts
+++ b/dev/src/plugins/index.ts
@@ -1,3 +1,6 @@
+// Framework
+import { Vuetify0DateAdapter } from '@vuetify/v0/date'
+
// Types
import type { App } from 'vue'
@@ -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({
//
diff --git a/packages/0/README.md b/packages/0/README.md
index b6c3f4fd8..940eebccb 100644
--- a/packages/0/README.md
+++ b/packages/0/README.md
@@ -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 |
diff --git a/packages/0/src/components/Alert/AlertAction.vue b/packages/0/src/components/Alert/AlertAction.vue
new file mode 100644
index 000000000..c4698f42e
--- /dev/null
+++ b/packages/0/src/components/Alert/AlertAction.vue
@@ -0,0 +1,80 @@
+/**
+ * @module AlertAction
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @remarks
+ * Dismiss button for alert banners. Calls dismiss() from the alert context when
+ * clicked, which sets v-model to false. Receives an accessible label from the
+ * locale system so it announces correctly to screen readers even when rendered
+ * as an icon-only button.
+ */
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/0/src/components/Alert/AlertDescription.vue b/packages/0/src/components/Alert/AlertDescription.vue
new file mode 100644
index 000000000..ec54d0d5e
--- /dev/null
+++ b/packages/0/src/components/Alert/AlertDescription.vue
@@ -0,0 +1,65 @@
+/**
+ * @module AlertDescription
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @remarks
+ * Description component for alert banners. Renders with the ID that Alert.Root
+ * references in its aria-describedby attribute, associating the body text with
+ * the alert container for screen readers.
+ */
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/0/src/components/Alert/AlertIcon.vue b/packages/0/src/components/Alert/AlertIcon.vue
new file mode 100644
index 000000000..692eb5c47
--- /dev/null
+++ b/packages/0/src/components/Alert/AlertIcon.vue
@@ -0,0 +1,68 @@
+/**
+ * @module AlertIcon
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @remarks
+ * Icon slot for alert banners. Renders with aria-hidden="true" by default because
+ * alert icons are decorative — the semantic meaning is carried by the text content.
+ * Pass aria-label to make the icon meaningful to screen readers when it communicates
+ * information beyond what the text already conveys.
+ */
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/0/src/components/Alert/AlertRoot.vue b/packages/0/src/components/Alert/AlertRoot.vue
new file mode 100644
index 000000000..3a6bbd891
--- /dev/null
+++ b/packages/0/src/components/Alert/AlertRoot.vue
@@ -0,0 +1,119 @@
+/**
+ * @module AlertRoot
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @remarks
+ * Root component for inline alert banners. Renders with `role="alert"` so screen
+ * readers announce the content automatically when it enters the DOM. Manages optional
+ * dismiss state via v-model — set to false to hide, or call dismiss() from slot props.
+ */
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/0/src/components/Alert/AlertTitle.vue b/packages/0/src/components/Alert/AlertTitle.vue
new file mode 100644
index 000000000..bb7895f8c
--- /dev/null
+++ b/packages/0/src/components/Alert/AlertTitle.vue
@@ -0,0 +1,65 @@
+/**
+ * @module AlertTitle
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @remarks
+ * Title component for alert banners. Renders with the ID that Alert.Root
+ * references in its aria-labelledby attribute, providing an accessible name
+ * for screen readers.
+ */
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/0/src/components/Alert/index.test.ts b/packages/0/src/components/Alert/index.test.ts
new file mode 100644
index 000000000..614c3eec4
--- /dev/null
+++ b/packages/0/src/components/Alert/index.test.ts
@@ -0,0 +1,256 @@
+import { describe, expect, it } from 'vitest'
+import { renderToString } from 'vue/server-renderer'
+
+// Utilities
+import { mount } from '@vue/test-utils'
+import { createSSRApp, defineComponent, h, ref } from 'vue'
+
+import { Alert } from './index'
+
+describe('alert', () => {
+ describe('root', () => {
+ it('should render as div by default', () => {
+ const wrapper = mount(Alert.Root)
+ expect(wrapper.element.tagName).toBe('DIV')
+ })
+
+ it('should have role=alert', () => {
+ const wrapper = mount(Alert.Root)
+ expect(wrapper.attributes('role')).toBe('alert')
+ })
+
+ it('should have aria-labelledby pointing to title', () => {
+ const wrapper = mount(Alert.Root, { props: { id: 'test-alert' } })
+ expect(wrapper.attributes('aria-labelledby')).toBe('test-alert-title')
+ })
+
+ it('should have aria-describedby pointing to description', () => {
+ const wrapper = mount(Alert.Root, { props: { id: 'test-alert' } })
+ expect(wrapper.attributes('aria-describedby')).toBe('test-alert-description')
+ })
+
+ it('should expose isDismissed=false and dismiss in slot props', () => {
+ let slotProps: any
+
+ mount(Alert.Root, {
+ slots: {
+ default: (props: any) => {
+ slotProps = props
+ return h('span', 'content')
+ },
+ },
+ })
+
+ expect(slotProps.isDismissed).toBe(false)
+ expect(typeof slotProps.dismiss).toBe('function')
+ expect(typeof slotProps.id).toBe('string')
+ })
+
+ it('should expose attrs with role, id, aria-labelledby, aria-describedby, data-state', () => {
+ let slotProps: any
+
+ mount(Alert.Root, {
+ props: { id: 'my-alert' },
+ slots: {
+ default: (props: any) => {
+ slotProps = props
+ return h('span', 'content')
+ },
+ },
+ })
+
+ expect(slotProps.attrs.role).toBe('alert')
+ expect(slotProps.attrs.id).toBe('my-alert')
+ expect(slotProps.attrs['aria-labelledby']).toBe('my-alert-title')
+ expect(slotProps.attrs['aria-describedby']).toBe('my-alert-description')
+ expect(slotProps.attrs['data-state']).toBe('visible')
+ })
+
+ it('should have data-state=visible by default', () => {
+ const wrapper = mount(Alert.Root)
+ expect(wrapper.attributes('data-state')).toBe('visible')
+ })
+
+ it('should have data-state=dismissed after dismiss', async () => {
+ const wrapper = mount(Alert.Root, {
+ slots: {
+ default: (props: any) => h('button', { onClick: props.dismiss }, 'Dismiss'),
+ },
+ })
+ await wrapper.find('button').trigger('click')
+ expect(wrapper.attributes('data-state')).toBe('dismissed')
+ })
+
+ it('should support v-model for visibility', async () => {
+ const visible = ref(true)
+
+ mount(Alert.Root, {
+ props: {
+ 'modelValue': visible.value,
+ 'onUpdate:modelValue': (v: unknown) => {
+ visible.value = v as boolean
+ },
+ },
+ slots: {
+ default: (props: any) => h('button', { onClick: props.dismiss }, 'Dismiss'),
+ },
+ })
+
+ expect(visible.value).toBe(true)
+ })
+
+ it('should set isDismissed=true and emit update:model-value=false when dismiss is called', async () => {
+ const visible = ref(true)
+
+ const wrapper = mount(Alert.Root, {
+ props: {
+ 'modelValue': visible.value,
+ 'onUpdate:modelValue': (v: unknown) => {
+ visible.value = v as boolean
+ },
+ },
+ slots: {
+ default: (props: any) => h('button', { onClick: props.dismiss }, 'Dismiss'),
+ },
+ })
+
+ await wrapper.find('button').trigger('click')
+ expect(visible.value).toBe(false)
+ })
+ })
+
+ describe('title', () => {
+ it('should render as p by default', () => {
+ const wrapper = mount(Alert.Root, {
+ slots: { default: () => h(Alert.Title, {}, () => 'Title') },
+ })
+ expect(wrapper.findComponent(Alert.Title as any).element.tagName).toBe('P')
+ })
+
+ it('should have correct id for aria-labelledby', () => {
+ const wrapper = mount(Alert.Root, {
+ props: { id: 'test-alert' },
+ slots: { default: () => h(Alert.Title, {}, () => 'Title') },
+ })
+ expect(wrapper.findComponent(Alert.Title as any).attributes('id')).toBe('test-alert-title')
+ })
+ })
+
+ describe('description', () => {
+ it('should render as p by default', () => {
+ const wrapper = mount(Alert.Root, {
+ slots: { default: () => h(Alert.Description, {}, () => 'Desc') },
+ })
+ expect(wrapper.findComponent(Alert.Description as any).element.tagName).toBe('P')
+ })
+
+ it('should have correct id for aria-describedby', () => {
+ const wrapper = mount(Alert.Root, {
+ props: { id: 'test-alert' },
+ slots: { default: () => h(Alert.Description, {}, () => 'Desc') },
+ })
+ expect(wrapper.findComponent(Alert.Description as any).attributes('id')).toBe('test-alert-description')
+ })
+ })
+
+ describe('icon', () => {
+ it('should render as span by default', () => {
+ const wrapper = mount(Alert.Root, {
+ slots: { default: () => h(Alert.Icon, {}, () => '⚠') },
+ })
+ expect(wrapper.findComponent(Alert.Icon as any).element.tagName).toBe('SPAN')
+ })
+
+ it('should have aria-hidden=true', () => {
+ const wrapper = mount(Alert.Root, {
+ slots: { default: () => h(Alert.Icon, {}, () => '⚠') },
+ })
+ expect(wrapper.findComponent(Alert.Icon as any).attributes('aria-hidden')).toBe('true')
+ })
+ })
+
+ describe('action', () => {
+ it('should render as button by default', () => {
+ const wrapper = mount(Alert.Root, {
+ slots: { default: () => h(Alert.Action, {}, () => '✕') },
+ })
+ expect(wrapper.findComponent(Alert.Action as any).element.tagName).toBe('BUTTON')
+ })
+
+ it('should have type=button', () => {
+ const wrapper = mount(Alert.Root, {
+ slots: { default: () => h(Alert.Action, {}, () => '✕') },
+ })
+ expect(wrapper.findComponent(Alert.Action as any).attributes('type')).toBe('button')
+ })
+
+ it('should have aria-label', () => {
+ const wrapper = mount(Alert.Root, {
+ slots: { default: () => h(Alert.Action, {}, () => '✕') },
+ })
+ expect(wrapper.findComponent(Alert.Action as any).attributes('aria-label')).toBeDefined()
+ })
+
+ it('should call dismiss on click', async () => {
+ const visible = ref(true)
+
+ const wrapper = mount(Alert.Root, {
+ props: {
+ 'modelValue': visible.value,
+ 'onUpdate:modelValue': (v: unknown) => {
+ visible.value = v as boolean
+ },
+ },
+ slots: { default: () => h(Alert.Action, {}, () => '✕') },
+ })
+
+ await wrapper.findComponent(Alert.Action as any).trigger('click')
+ expect(visible.value).toBe(false)
+ })
+ })
+
+ describe('integration', () => {
+ it('should wire title, description, icon, and action together', () => {
+ const wrapper = mount(Alert.Root, {
+ props: { id: 'full-alert' },
+ slots: {
+ default: () => [
+ h(Alert.Icon, {}, () => '⚠'),
+ h(Alert.Title, {}, () => 'Warning'),
+ h(Alert.Description, {}, () => 'Something needs attention.'),
+ h(Alert.Action, {}, () => '✕'),
+ ],
+ },
+ })
+
+ expect(wrapper.attributes('role')).toBe('alert')
+ expect(wrapper.attributes('aria-labelledby')).toBe('full-alert-title')
+ expect(wrapper.attributes('aria-describedby')).toBe('full-alert-description')
+ expect(wrapper.findComponent(Alert.Title as any).attributes('id')).toBe('full-alert-title')
+ expect(wrapper.findComponent(Alert.Description as any).attributes('id')).toBe('full-alert-description')
+ })
+ })
+})
+
+describe('alert SSR', () => {
+ it('should render to string on server without errors', async () => {
+ const app = createSSRApp(defineComponent({
+ render: () =>
+ h(Alert.Root as never, { id: 'ssr-alert' }, {
+ default: () => [
+ h(Alert.Icon as never, {}, () => '⚠'),
+ h(Alert.Title as never, {}, () => 'Server-side alert'),
+ h(Alert.Description as never, {}, () => 'This was rendered on the server.'),
+ h(Alert.Action as never, {}, () => '✕'),
+ ],
+ }),
+ }))
+
+ const html = await renderToString(app)
+
+ expect(html).toBeTruthy()
+ expect(html).toContain('role="alert"')
+ expect(html).toContain('Server-side alert')
+ expect(html).toContain('This was rendered on the server.')
+ })
+})
diff --git a/packages/0/src/components/Alert/index.ts b/packages/0/src/components/Alert/index.ts
new file mode 100644
index 000000000..6f67d0729
--- /dev/null
+++ b/packages/0/src/components/Alert/index.ts
@@ -0,0 +1,110 @@
+export { default as AlertAction } from './AlertAction.vue'
+export { default as AlertDescription } from './AlertDescription.vue'
+export { default as AlertIcon } from './AlertIcon.vue'
+export { default as AlertRoot } from './AlertRoot.vue'
+export { default as AlertTitle } from './AlertTitle.vue'
+export { provideAlertContext, useAlertContext } from './AlertRoot.vue'
+export type { AlertActionProps, AlertActionSlotProps } from './AlertAction.vue'
+export type { AlertDescriptionProps, AlertDescriptionSlotProps } from './AlertDescription.vue'
+export type { AlertIconProps, AlertIconSlotProps } from './AlertIcon.vue'
+export type { AlertContext, AlertRootProps, AlertRootSlotProps } from './AlertRoot.vue'
+export type { AlertTitleProps, AlertTitleSlotProps } from './AlertTitle.vue'
+
+// Components
+import Action from './AlertAction.vue'
+import Description from './AlertDescription.vue'
+import Icon from './AlertIcon.vue'
+import Root from './AlertRoot.vue'
+import Title from './AlertTitle.vue'
+
+/**
+ * Alert component for inline status banners with title, description, icon, and dismiss action.
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @example
+ * ```vue
+ *
+ *
+ *
+ *
+ * ⚠️
+ * Heads up
+ * Your session expires in 5 minutes.
+ * ✕
+ *
+ *
+ * ```
+ */
+export const Alert = {
+ /**
+ * Root container for the alert banner. Renders with role="alert".
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @example
+ * ```vue
+ *
+ *
+ *
+ *
+ * Notice
+ *
+ *
+ *
+ * ```
+ */
+ Root,
+ /**
+ * Title element providing an accessible name for the alert.
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @example
+ * ```vue
+ * Something went wrong
+ * ```
+ */
+ Title,
+ /**
+ * Body text of the alert, associated via aria-describedby.
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @example
+ * ```vue
+ * Check your connection and try again.
+ * ```
+ */
+ Description,
+ /**
+ * Decorative icon or avatar area. Renders aria-hidden="true" by default.
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @example
+ * ```vue
+ *
+ *
+ *
+ * ```
+ */
+ Icon,
+ /**
+ * Dismiss button that calls dismiss() from the alert context.
+ *
+ * @see https://0.vuetifyjs.com/components/semantic/alert
+ *
+ * @example
+ * ```vue
+ * ✕
+ * ```
+ */
+ Action,
+}
diff --git a/packages/0/src/components/index.ts b/packages/0/src/components/index.ts
index ad5f4a9ec..73bd57f83 100644
--- a/packages/0/src/components/index.ts
+++ b/packages/0/src/components/index.ts
@@ -1,3 +1,4 @@
+export * from './Alert'
export * from './AlertDialog'
export * from './AspectRatio'
export * from './Atom'
diff --git a/packages/0/src/maturity.json b/packages/0/src/maturity.json
index 6dd5e761e..c84aebb7e 100644
--- a/packages/0/src/maturity.json
+++ b/packages/0/src/maturity.json
@@ -483,7 +483,8 @@
"category": "disclosure"
},
"Alert": {
- "level": "draft",
+ "level": "preview",
+ "since": null,
"category": "semantic"
},
"Avatar": {