Alert
Messages in the page for info, success, warning and error, each with an icon and clear words. Tones come from the kit tokens.
One of 9 components in Overlays
Details
What Alert brings along, read from the registry and the source.
- Category
- Overlays
- Area
- Elements
- Variants
- 2
- Version
- 1.0.0 · since 1.0.0
- File
- components/ui/alert.tsx
- npm packages
- None
- Languages
- English and German, 1 text of its own
- Studio
- Adjust in code
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The AlertProps interface from components/ui/alert.tsx, with the comments from the code.
toneoptionalAlertToneneutralinfosuccesswarningerror
titleoptionalReact.ReactNodechildrenoptionalReact.ReactNodeiconoptionalIconNameOverride the tone icon
actionsoptionalReact.ReactNodeButtons or links under the text (or at the end in the inline layout)
dismissibleoptionalbooleanShows a dismiss button. The alert hides itself and calls onDismiss.
onDismissoptional() => voidvariantoptional'soft' | 'outline' | 'accent' | 'inline'soft: tinted surface (default). outline: card with a colored icon. accent: a bar on the start edge. inline: one compact line.
Built-in texts
Alert writes these texts itself. The language follows the KitProvider, the labels prop replaces single ones.
Show all 1 texts
| Key | English | German |
|---|---|---|
| dismiss | English Dismiss | German Ausblenden |
Accessibility
Read from the source of the component.
- Sets aria-label, so screen readers announce purpose and state.
- Uses the roles alert, status and urgent.
- Announces changes through a live region without moving focus.
- The server renders the starting state in full, JavaScript only adds the interaction.
Motion
- Brings animations of its own, calmly timed and without wobble.
- With reduced motion (prefers-reduced-motion) it stays still. 2 places in the code switch motion off then.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 141 lines
'use client';
import { useRef, useState } from 'react';
import { cn, prefersReducedMotion, useInView, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
const TEXTS = { en: { dismiss: 'Dismiss' }, de: { dismiss: 'Ausblenden' } };
/* The icon pops in once the alert is in view */
const CSS =
'.kitfx-alert[data-in] .kitfx-alert-icon{animation:kitfx-alert-icon .6s cubic-bezier(.34,1.56,.64,1) .12s both}@keyframes kitfx-alert-icon{from{opacity:0;transform:scale(.5)}}' +
'@media (prefers-reduced-motion:reduce){.kitfx-alert .kitfx-alert-icon{animation:none}}';
export type AlertTone = 'neutral' | 'info' | 'success' | 'warning' | 'error';
/* One color per tone, everything else is mixed from it, so both modes and every accent stay in tune */
const TONE: Record<AlertTone, { color: string; icon: IconName }> = {
neutral: { color: 'var(--muted-foreground)', icon: 'info' },
info: { color: 'var(--info)', icon: 'info' },
success: { color: 'var(--success)', icon: 'check-circle' },
warning: { color: 'var(--warning)', icon: 'alert' },
error: { color: 'var(--destructive)', icon: 'alert-circle' },
};
export interface AlertProps {
tone?: AlertTone;
title?: React.ReactNode;
children?: React.ReactNode;
/** Override the tone icon */
icon?: IconName;
/** Buttons or links under the text (or at the end in the inline layout) */
actions?: React.ReactNode;
/** Shows a dismiss button. The alert hides itself and calls onDismiss. */
dismissible?: boolean;
onDismiss?: () => void;
/** soft: tinted surface (default). outline: card with a colored icon. accent: a bar on the start edge. inline: one compact line. */
variant?: 'soft' | 'outline' | 'accent' | 'inline';
labels?: Partial<(typeof TEXTS)['en']>;
className?: string;
}
/**
* A message in the flow of the page: info, success, warning or error. Always text and icon, never color alone.
* Errors and warnings use role="alert", the others role="status".
* <Alert tone="warning" title="Your trial ends in 3 days">Add a payment method to keep your projects.</Alert>
*/
export function Alert({ tone = 'info', title, children, icon, actions, dismissible, onDismiss, variant = 'soft', labels, className }: AlertProps) {
const t = useTexts(TEXTS, labels);
const [open, setOpen] = useState(true);
const [ref, inView] = useInView<HTMLDivElement>();
const leaving = useRef(false);
if (!open) return null;
const c = TONE[tone];
const urgent = tone === 'error' || tone === 'warning';
const ink = `color-mix(in oklab, ${c.color} 78%, var(--foreground))`;
const surface = {
soft: 'border border-[color-mix(in_oklab,var(--tone)_28%,var(--border))] bg-[color-mix(in_oklab,var(--tone)_8%,var(--background))]',
outline: 'border border-border bg-card shadow-[var(--highlight),var(--shadow-sm)]',
accent: 'border border-border border-s-[3px] border-s-[var(--tone)] bg-card shadow-[var(--shadow-sm)]',
inline: 'border border-[color-mix(in_oklab,var(--tone)_24%,var(--border))] bg-[color-mix(in_oklab,var(--tone)_7%,var(--background))]',
}[variant];
const dismiss = dismissible && (
<button
type="button"
aria-label={t.dismiss}
onClick={() => {
const el = ref.current;
const done = () => {
setOpen(false);
onDismiss?.();
};
if (!el || prefersReducedMotion()) return done();
if (leaving.current) return;
leaving.current = true;
// Out first, then the space closes, so the content below glides up instead of jumping
const h = el.offsetHeight;
el.style.pointerEvents = 'none';
el.style.overflow = 'hidden';
el.animate([{ opacity: 1, transform: 'none' }, { opacity: 0, transform: 'translateX(12px) scale(0.98)' }], { duration: 220, easing: 'cubic-bezier(0.4, 0, 0.9, 0.6)', fill: 'forwards' })
.finished.then(() => el.animate([{ height: `${h}px`, marginBlock: getComputedStyle(el).marginBlock }, { height: '0px', marginBlock: '0px', paddingBlock: '0px', borderWidth: '0px' }], { duration: 240, easing: 'cubic-bezier(0.2, 0.7, 0.2, 1)', fill: 'forwards' }).finished)
.then(done, done);
}}
className={cn('group/close grid size-8 shrink-0 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-[color-mix(in_oklab,var(--tone)_12%,transparent)] hover:text-foreground', variant !== 'inline' && '-me-1.5 -mt-1.5')}
>
<Icon name="x" size={15} className="transition-transform duration-300 ease-[var(--ease-kit)] group-hover/close:rotate-90 motion-reduce:transition-none" />
</button>
);
// Rises into place when it scrolls into view, the icon pops
const motion = cn('kitfx-alert transition-[opacity,translate] duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none', !inView && 'translate-y-3 opacity-0');
const style = <style href="kitfx-alert" precedence="default">{CSS}</style>;
if (variant === 'inline') {
return (
<div ref={ref} data-in={inView || undefined} role={urgent ? 'alert' : 'status'} className={cn('flex min-h-11 items-center gap-2.5 rounded-lg px-3 py-2 text-sm', surface, motion, className)} style={{ '--tone': c.color } as React.CSSProperties}>
{style}
<Icon name={icon ?? c.icon} size={16} className="kitfx-alert-icon shrink-0" style={{ color: ink }} />
<p className="min-w-0 flex-1 text-pretty text-foreground">
{title && <span className="font-medium">{title} </span>}
<span className="text-muted-foreground">{children}</span>
</p>
{actions && <div className="flex shrink-0 items-center gap-2 text-[13px] font-medium">{actions}</div>}
{dismiss}
</div>
);
}
return (
<div ref={ref} data-in={inView || undefined} role={urgent ? 'alert' : 'status'} className={cn('relative flex gap-3 rounded-xl p-4', surface, motion, className)} style={{ '--tone': c.color } as React.CSSProperties}>
{style}
<span
className={cn('kitfx-alert-icon grid shrink-0 place-items-center', variant === 'outline' ? 'size-8 rounded-lg bg-[color-mix(in_oklab,var(--tone)_12%,transparent)]' : 'size-5 mt-px')}
style={{ color: ink }}
>
<Icon name={icon ?? c.icon} size={variant === 'outline' ? 17 : 18} />
</span>
<div className={cn('min-w-0 flex-1', variant === 'outline' && 'pt-[5px]')}>
{title && <p className="text-sm font-semibold leading-5 text-foreground">{title}</p>}
{children && <div className={cn('text-pretty text-sm leading-relaxed text-muted-foreground', title && 'mt-1')}>{children}</div>}
{actions && <div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-[13px] font-medium">{actions}</div>}
</div>
{dismiss}
</div>
);
}
/** A text link styled for alert actions */
export function AlertLink({ href, onClick, children }: { href?: string; onClick?: () => void; children: React.ReactNode }) {
const cls = 'inline-flex items-center gap-1 text-foreground underline decoration-[color-mix(in_oklab,var(--tone,var(--foreground))_45%,transparent)] underline-offset-4 transition-[text-decoration-color] duration-200 hover:decoration-current';
return href ? (
<a href={href} className={cls}>
{children}
</a>
) : (
<button type="button" onClick={onClick} className={cls}>
{children}
</button>
);
}Frequent questions
How do I install Alert?
With aiw add @ai-website/alert. The command puts components/ui/alert.tsx into your project, the code is yours from then on. It brings along icon and toast. It needs no further npm packages.
Which variants does Alert have?
2 variants, Tones and Styles.
Can I translate the texts of Alert?
Yes. Alert carries 1 texts of its own in English and German, such as “Dismiss”. The language follows the KitProvider, single texts change through the labels prop.
Can an AI assistant build with Alert?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Alert with aiw_components and install it with aiw_add @ai-website/alert. Name the variant in your prompt, for example variant="outline|accent|inline", and the assistant writes your content straight into the props.