Skip to content

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.

Preview Live · Vue
Usage
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>
jsx
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>
  )
}
svelte
<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
Alert.vue
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.

Terminal
npx klean-ui add alert

Source you own. Edit the installed files to make them yours.

Usage ​

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 ​

InputDefaultPurpose
asdivNative 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.

SituationMarkup
Static guidance already present with the pageNo role
Supporting information that is useful but not a live updaterole="note" when the extra semantic is worthwhile
A non-urgent result inserted after an operationrole="status" or an existing polite live region
An urgent failure inserted after the user actsrole="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.

Preview Live · Vue
View source DeploymentChecklist.vue
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.

Preview Live · Vue
View source AlertRecipes.vue
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.

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>
jsx
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
svelte
<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>
  • Button — supplies truthful actions and navigation inside an Alert.
  • Toast — handles transient queued notifications.
  • Dialog — handles focused modal decisions.
  • Input — keeps field-level errors and relationships close to the field.

All open source projects are released under the MIT License.