reelcn
GitHubBack to landing

Toast

Notification card with a status icon that slides in from an edge, holds, then slides back out.

Toast, success
Toast, success thumbnail, 16:9

Installation

npx shadcn add https://www.reelcn.dev/r/toast.json

Files land in src/reelcn/ with the items it builds on.

Usage

<Toast variant="success" title="Deployed" description="Live in 12 regions" />

Props

PropTypeDescription
delaynumberFrames to wait before entering.
durationnumberEnter duration in frames. Defaults to 0.6s. Some items reuse duration for an unrelated local timing. See that item's own prop docs.
exitnumber | booleanExit at the end of the parent `<Sequence>`. false keeps it on screen, a number sets the exit length in frames.
motionMotionPersonalityOverride the theme's motion personality.
posterbooleanRenders fully entered, with no exit: for a still frame or thumbnail. Defaults to false.
holdFramesnumberDelays the start of the exit window by this many frames, for a minimum on-screen hold. Defaults to 0.
variantToastVariant Default "info".
title*string
descriptionstring
edge"top" | "top-right" | "bottom" | "bottom-right"Edge it slides from. Defaults to top in portrait, bottom-right otherwise.
widthnumberCard width in design units. Default 380.
backgroundstring
borderColorstring
titleColorstring
descriptionColorstring
accentColorstringIcon well and stroke color. Defaults per variant.
radiusnumberCorner radius in design units. Defaults to 70% of the theme radius.
styleCSSProperties
classNamestring

Built from

Use

  • Confirming an action, a deploy, an upload or a saved state in a product demo
  • A short system message that appears over a UI mockup

Avoid

  • A message pointing at part of the screen. Use callout

Guides

Components take their colors from a theme, their timing from motion props and their layout from the canvas.

Source

registry/items/toast.tsx
/**
 * @title Toast
 * @category product
 * @description Notification card with a status icon that slides in from an edge, holds, then slides back out.
 * @duration 90
 * @use Confirming an action, a deploy, an upload or a saved state in a product demo
 * @use A short system message that appears over a UI mockup
 * @avoid A message pointing at part of the screen. Use `callout`
 * @tags toast, notification, alert, snackbar, status
 * @example
 * <Toast variant="success" title="Deployed" description="Live in 12 regions" />
 */
import type React from "react";
import { alpha, clamp01, type MotionProps, useMotion, useTheme, useViewport } from "./core";

export type ToastVariant = "success" | "info" | "warning" | "error";

export type ToastProps = MotionProps & {
  variant?: ToastVariant;
  title: string;
  description?: string;
  /** Edge it slides from. Defaults to `top` in portrait, `bottom-right` otherwise. */
  edge?: "top" | "bottom" | "bottom-right" | "top-right";
  /** Card width in design units. */
  width?: number;
  background?: string;
  borderColor?: string;
  titleColor?: string;
  descriptionColor?: string;
  /** Icon well and stroke color. Defaults per `variant`. */
  accentColor?: string;
  /** Corner radius in design units. Defaults to 70% of the theme radius. */
  radius?: number;
  style?: React.CSSProperties;
  className?: string;
};

function ToastIcon({ variant }: { variant: ToastVariant }) {
  if (variant === "success") return <path d="M5 13l4 4L19 7" />;
  if (variant === "info")
    return (
      <>
        <circle cx="12" cy="12" r="9" />
        <path d="M12 11v5M12 8h.01" />
      </>
    );
  if (variant === "warning")
    return (
      <>
        <path d="M10.29 3.86 1.82 18a1 1 0 0 0 .86 1.5h18.64a1 1 0 0 0 .86-1.5L13.71 3.86a1 1 0 0 0-1.72 0Z" />
        <path d="M12 9v4M12 17h.01" />
      </>
    );
  return (
    <>
      <circle cx="12" cy="12" r="9" />
      <path d="M15 9l-6 6M9 9l6 6" />
    </>
  );
}

export function Toast({
  variant = "info",
  title,
  description,
  edge,
  width = 380,
  background,
  borderColor,
  titleColor,
  descriptionColor,
  accentColor,
  radius,
  style,
  className,
  ...motion
}: ToastProps) {
  const theme = useTheme();
  const { u, safe, isPortrait } = useViewport();
  const m = useMotion(motion);
  const side = edge ?? (isPortrait ? "top" : "bottom-right");
  const defaultTone: Record<ToastVariant, string> = {
    success: theme.colors.success,
    info: theme.colors.accent,
    warning: theme.colors.warning,
    error: theme.colors.danger,
  };
  const tone = accentColor ?? defaultTone[variant];
  const border = borderColor ?? theme.colors.border;
  const cardW = u(width);
  const enter = clamp01(m.enter);
  const away = 1 - enter + m.exit;
  const centered = side === "top" || side === "bottom";
  const dir = side === "top" ? -1 : 1;
  const travel = u(70);
  const slide = dir * away * travel;
  const translate = centered ? `-50% ${slide}px` : `${slide}px 0`;
  const position: React.CSSProperties =
    side === "top"
      ? { top: safe.top, left: "50%" }
      : side === "bottom"
        ? { bottom: safe.bottom, left: "50%" }
        : side === "top-right"
          ? { top: safe.top, right: safe.x }
          : { bottom: safe.bottom, right: safe.x };

  return (
    <div
      className={className}
      style={{
        position: "absolute",
        ...position,
        width: cardW,
        display: "flex",
        gap: u(14),
        padding: u(18),
        borderRadius: u(radius ?? theme.radius * 0.7),
        background: background ?? theme.colors.surface,
        border: `1px solid ${border}`,
        boxShadow: `0 ${u(16)}px ${u(44)}px ${alpha(theme.colors.shadow, 0.28)}`,
        opacity: enter * (1 - m.exit),
        translate,
        fontFamily: theme.fonts.body,
        ...style,
      }}
    >
      <div
        style={{
          flexShrink: 0,
          width: u(34),
          height: u(34),
          borderRadius: "50%",
          display: "grid",
          placeItems: "center",
          background: alpha(tone, 0.16),
          color: tone,
        }}
      >
        <svg
          aria-hidden="true"
          viewBox="0 0 24 24"
          width={u(18)}
          height={u(18)}
          fill="none"
          stroke="currentColor"
          strokeWidth={2.4}
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <ToastIcon variant={variant} />
        </svg>
      </div>
      <div>
        <div style={{ fontSize: u(24), fontWeight: 600, color: titleColor ?? theme.colors.foreground }}>{title}</div>
        {description && (
          <div style={{ marginTop: u(4), fontSize: u(20), color: descriptionColor ?? theme.colors.muted }}>
            {description}
          </div>
        )}
      </div>
    </div>
  );
}

On this page