Skip to content
UI Kit
Accent color

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

2 variants

Variants

Tones

Soft surfaces with title, text and actions. They rise in one after another, a dismissed alert slides out and the space closes softly.

Styles

Outline for cards, a bar on the edge for long pages, one compact line for forms and toolbars.

Details

What Alert brings along, read from the registry and the source.

Category
Overlays
Variants
2
Version
1.0.0 · since 1.0.0
File
components/ui/alert.tsx
Brings along
utilskiticontoast
npm packages
None
Languages
English and German, 1 text of its own
Studio
Adjust in code

Props

The AlertProps interface from components/ui/alert.tsx, with the comments from the code.

  • toneoptional
    AlertTone

    neutralinfosuccesswarningerror

  • titleoptional
    React.ReactNode
  • childrenoptional
    React.ReactNode
  • iconoptional
    IconName

    Override the tone icon

  • actionsoptional
    React.ReactNode

    Buttons or links under the text (or at the end in the inline layout)

  • dismissibleoptional
    boolean

    Shows a dismiss button. The alert hides itself and calls onDismiss.

  • onDismissoptional
    () => void
  • variantoptional
    '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
dismissEnglish DismissGerman 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
components/ui/alert.tsx
'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.

Often searched

Jump to

Copied to the clipboard