# Toast

> Transient notifications. `info`, `success`, `warning`, and `danger` variants. Shares a single <Toaster /> provider per app and fires via `toaster.create(...)`.

- Category: overlay
- Status: stable (since 1.0.0)
- A11y pattern: https://www.w3.org/WAI/ARIA/apg/patterns/alert/
- Tokens: --background-quaternary, --foreground-primary, --foreground-secondary, --highlight-color, --success-color, --warning-color, --danger-color
- Playground: https://design.freecodecamp.org/playground#toast
- npm dependencies: `react@>=18 <20`, `@ark-ui/react@^5.0.0`
- Registry dependencies: [theme](https://design.freecodecamp.org/registry/theme.md)
- Files:
  - `Toast.tsx` → `src/ui/toast/Toast.tsx` (raw: https://design.freecodecamp.org/registry/toast/Toast.tsx)
  - `toast.css` → `src/ui/toast/toast.css` (raw: https://design.freecodecamp.org/registry/toast/toast.css)

## Install (copy source)

1. Ensure the theme is installed once per project - tokens.css + base.css imported globally, fonts available. See https://design.freecodecamp.org/registry/theme.md and https://design.freecodecamp.org/registry/starter.md.
2. Copy the files below into `src/ui/toast/` (adjust to your project layout) and import the CSS once from your global stylesheet, e.g. `@import './ui/toast/toast.css';`.
3. Colors, spacing and type come from tokens - tailor the component by editing the copied source; recolour by editing tokens.css, not the component CSS.

## Usage

Use Toast for feedback that is time-bound, does not demand a decision,
and can safely disappear. For interruptions that block a flow, reach
for `<Modal>`. Toasts are stack-aware: multiple overlap without
flattening, and auto-dismiss after five seconds by default.

## Usage

```tsx
import { Toaster, createToaster } from './ui/toast/Toast';
// Create once at module scope - the store is a singleton.
export const toaster = createToaster({ placement: 'top-end' });

// Near the app root:
<Toaster toaster={toaster} />;

// Anywhere a handler wants to flash a toast:
toaster.create({
  type: 'success',
  title: 'Saved',
  description: 'Profile updated.'
});
```

### Vanilla JS

For non-React pages, mark the container and triggers with `data-uikit-*`
hooks. The IIFE bundle wires them on load - no imports needed.

```html
<div data-uikit-toaster class="toaster"></div>

<button
  data-uikit-toast-trigger
  data-toast-type="success"
  data-toast-title="Saved"
  data-toast-description="Profile updated"
>
  Save
</button>
```

## Props - `<Toast>`

The presentational item. Use it directly for custom integrations; the
`<Toaster>` default renderer wires it up for you.

## Accessibility

`info | success | warning` toasts render `role="status"` + `aria-live="polite"`
so they are announced but do not interrupt. `danger` toasts upgrade to
`role="alert"` so assistive tech interrupts for errors. Every toast owns
a labelled close button - do not remove it unless the toast is strictly
informational and short-lived.

## Example

```tsx
import { Toast, Toaster, createToaster } from './ui/toast/Toast';

// 1) Static - render Toast directly (the showcase variant).
<Toast variant='success' title='Saved' description='Synced.' />

// 2) Dynamic - drive a stack via createToaster + <Toaster>.
const toaster = createToaster({});

<Toaster toaster={toaster} />
toaster.create({ title: 'Saved', type: 'success' });
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `variant` | `enum` | no | `info` |  |
| `title` | `ReactNode` | no | - |  |
| `description` | `ReactNode` | no | - |  |
| `dismissible` | `boolean` | no | `true` | Emit a close (`×`) button wired to `onDismiss`. Default: true. |
| `onDismiss` | `(() => void)` | no | - |  |

## Source: Toast.tsx

```tsx
import React, { forwardRef } from 'react';
import {
  Toast as ArkToast,
  Toaster as ArkToaster,
  createToaster as arkCreateToaster,
  type CreateToasterProps,
  type CreateToasterReturn,
  type ToastOptions
} from '@ark-ui/react/toast';

export type ToastVariant = 'info' | 'success' | 'warning' | 'danger';

export interface ToastProps extends Omit<
  React.HTMLAttributes<HTMLDivElement>,
  'title'
> {
  variant?: ToastVariant;
  title?: React.ReactNode;
  description?: React.ReactNode;
  /** Emit a close (`×`) button wired to `onDismiss`. Default: true. */
  dismissible?: boolean;
  onDismiss?: () => void;
}

export const Toast = forwardRef<HTMLDivElement, ToastProps>(
  (
    {
      variant = 'info',
      title,
      description,
      dismissible = true,
      onDismiss,
      className = '',
      children,
      ...rest
    },
    ref
  ) => {
    const classes = ['toast', `toast--${variant}`, className]
      .filter(Boolean)
      .join(' ');
    const role = variant === 'danger' ? 'alert' : 'status';
    return (
      <div ref={ref} role={role} className={classes} {...rest}>
        <div className='toast__body'>
          {title !== undefined && <p className='toast__title'>{title}</p>}
          {description !== undefined && (
            <p className='toast__description'>{description}</p>
          )}
          {children}
        </div>
        {dismissible && (
          <button
            type='button'
            className='toast__close'
            aria-label='Dismiss'
            onClick={onDismiss}
          >
            {'×'}
          </button>
        )}
      </div>
    );
  }
);
Toast.displayName = 'Toast';

export type { CreateToasterProps, CreateToasterReturn };

export const createToaster = (props: CreateToasterProps): CreateToasterReturn =>
  arkCreateToaster({
    placement: 'top-end',
    overlap: true,
    gap: 16,
    duration: 5000,
    ...props
  });

export interface ToasterProps {
  toaster: CreateToasterReturn;
  /** Override the item renderer. Defaults to fCC <Toast> shell. */
  children?: (options: ToastOptions) => React.ReactNode;
  className?: string;
}

export const Toaster = ({
  toaster,
  children,
  className = ''
}: ToasterProps): React.JSX.Element => {
  const classes = ['toaster', className].filter(Boolean).join(' ');
  const renderItem =
    children ??
    ((options: ToastOptions) => {
      const variant: ToastVariant = isVariant(options.type)
        ? options.type
        : 'info';
      return (
        <ArkToast.Root>
          <Toast
            variant={variant}
            title={
              options.title !== undefined ? (
                <ArkToast.Title asChild>
                  <span>{options.title}</span>
                </ArkToast.Title>
              ) : undefined
            }
            description={
              options.description !== undefined ? (
                <ArkToast.Description asChild>
                  <span>{options.description}</span>
                </ArkToast.Description>
              ) : undefined
            }
            dismissible={false}
          />
          <ArkToast.CloseTrigger className='toast__close' aria-label='Dismiss'>
            {'×'}
          </ArkToast.CloseTrigger>
        </ArkToast.Root>
      );
    });
  return (
    <ArkToaster toaster={toaster} className={classes}>
      {renderItem}
    </ArkToaster>
  );
};
Toaster.displayName = 'Toaster';

const VARIANTS: ReadonlySet<ToastVariant> = new Set<ToastVariant>([
  'info',
  'success',
  'warning',
  'danger'
]);
const isVariant = (t: unknown): t is ToastVariant =>
  typeof t === 'string' && VARIANTS.has(t as ToastVariant);
```

## Source: toast.css

```css
.toaster {
  position: fixed;
  top: 16px;
  right: 16px;
  z-index: 9000;
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: min(360px, calc(100vw - 32px));
  pointer-events: none;
}
.toaster > * {
  pointer-events: auto;
}
.toast {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 14px;
  background: var(--background-quaternary);
  color: var(--foreground-primary);
  border: var(--border-width-thin) solid var(--foreground-secondary);
  border-left-width: 4px;
  border-left-color: var(--highlight-color);
  box-shadow:
    0 1px 0 rgba(0, 0, 0, 0.25),
    0 8px 24px rgba(0, 0, 0, 0.25);
  transition:
    transform 160ms ease-out,
    opacity 160ms ease-out;
}
.toast[data-state='open'] {
  animation: toast-enter 180ms ease-out;
}
.toast[data-state='closed'] {
  animation: toast-exit 160ms ease-in forwards;
}
.toast--info {
  border-left-color: var(--highlight-color);
}
.toast--success {
  border-left-color: var(--success-color);
}
.toast--warning {
  border-left-color: var(--warning-color);
}
.toast--danger {
  border-left-color: var(--danger-color);
}
.toast__body {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.toast__title {
  margin: 0;
  font-weight: 600;
  font-size: var(--fs-sm);
  line-height: 1.3;
}
.toast__description {
  margin: 0;
  font-size: var(--fs-xs);
  line-height: 1.45;
  color: var(--foreground-secondary);
}
.toast__close {
  flex: 0 0 auto;
  appearance: none;
  background: transparent;
  border: 0;
  padding: 0 4px;
  font-size: var(--fs-md);
  line-height: 1;
  color: var(--foreground-secondary);
  cursor: pointer;
}
.toast__close:hover,
.toast__close:focus-visible {
  color: var(--foreground-primary);
  outline: none;
}

@keyframes toast-enter {
  from {
    transform: translateY(-12px);
    opacity: 0;
  }
  to {
    transform: translateY(0);
    opacity: 1;
  }
}
@keyframes toast-exit {
  from {
    transform: translateY(0);
    opacity: 1;
  }
  to {
    transform: translateY(-12px);
    opacity: 0;
  }
}
@media (prefers-reduced-motion: reduce) {
  .toast[data-state='open'],
  .toast[data-state='closed'] {
    animation: none;
  }
}
```

## HTML / vanilla variant

```html
<div class="toaster">
  <div class="toast toast--success">
    <div class="toast__title">Saved</div>
    <div class="toast__description">Your progress is synced.</div>
    <button class="toast__close">×</button>
  </div>
</div>
```

Interactive behaviours for plain HTML come from the vanilla runtime (data-uikit-* attributes): https://design.freecodecamp.org/registry/vanilla.md - or download https://design.freecodecamp.org/cdn/uikit.global.js once and self-host it (do not hotlink).

## For coding agents

This library is distributed as copyable source, not an npm package. Start at https://design.freecodecamp.org/registry/starter.md, discover components via https://design.freecodecamp.org/llms.txt, and copy files into the consuming project. Keep token names intact; recolour by editing the copied tokens.css.
