Overlay

Toast

Notificación flotante apilable con expiración

Preview

color
position
children
onDismiss

Code

import css from "./Toast.module.css";
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";

import { XIcon } from "@phosphor-icons/react/dist/csr/X";

type Color = "success" | "error" | "info";
type Position =
  | "top-left"
  | "top-center"
  | "top-right"
  | "bottom-left"
  | "bottom-center"
  | "bottom-right";

interface ToastProps {
  color: Color;
  position?: Position;
  dismissAriaLabel?: string;
  children: React.ReactNode;
  onDismiss?: () => void;
}

export default function Toast({
  color,
  position,
  dismissAriaLabel,
  children,
  onDismiss,
}: ToastProps) {
  // No <body> to portal into until the client has it; in flow there is nothing to wait for.
  const [mounted, setMounted] = useState(false);

  useEffect(() => setMounted(true), []);

  // The toast is its own live region; a viewport stacking these must not wrap them in another.
  const card = (
    <div
      className={`
        ${css.toast}
        ${css[color]}
        ${position ? `${css.float} ${css[position.replace("-", "_")]}` : ""}
      `}
      role={color === "error" ? "alert" : "status"}
      style={
        { "--toast-color": `var(--color-${color})` } as React.CSSProperties
      }
    >
      <div className={css.toast_content}>{children}</div>

      {onDismiss && (
        <button
          type="button"
          className={css.toast_dismiss}
          aria-label={dismissAriaLabel}
          onClick={onDismiss}
        >
          <XIcon weight="bold" />
        </button>
      )}
    </div>
  );

  if (!position) return card;
  if (!mounted) return null;

  return createPortal(card, document.body);
}

Props

PropTypeRequiredDescription
color"success" | "error" | "info"requiredEstado semántico que tiñe el toast con la paleta de estado de Foundations. error se anuncia con role="alert" y el resto con role="status".
position"top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"—Esquina o centro donde flota por su cuenta; sin ella la tarjeta se renderiza en el flujo (es como ToastProvider la apila).
dismissAriaLabelstring—Junto a onDismiss: nombre del botón de cierre para lectores de pantalla.
childrenReact.ReactNoderequiredContenido de la notificación.
onDismiss() => void—Callback de la X; sin él no se dibuja.

Imperative API

Azúcar opcional sobre el <Toast> declarativo. Envuelve tu app en ToastProvider y lánzalo con una llamada.

púlsalo varias veces

position
duration
color
dismissAriaLabel

Props

PropTypeRequiredDescription
position"top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"requiredProp de ToastProvider. Esquina o centro donde vive la pila.
maxVisiblenumberrequiredProp de ToastProvider. Tope de toasts visibles; al superarse, el más antiguo se desahucia.
color"success" | "error" | "info"requiredEstado semántico que tiñe el toast con la paleta de estado de Foundations. error se anuncia con role="alert" y el resto con role="status".
contentReact.ReactNoderequiredContenido de la notificación.
durationnumber—Milisegundos hasta autoexpirar; sin ella no expira y solo lo retiran la X, el dismisser o maxVisible.
dismissAriaLabelstring—Nombre del botón de cierre para lectores de pantalla; sin él no se dibuja la X.
→ show()() => void—Devuelve el dismisser del toast lanzado; llamarlo lo retira (idempotente).