Ir al contenido
Evaluar mi proyecto
Componentes · Modales y capasDisponible

Snackbar Material Design 3

Aviso breve con acción Deshacer; el cierre automático se pausa al apuntar o enfocar y se anuncia por región viva.

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

capa-snackbar.tsx86 líneas
Descargar
"use client";

import { useEffect, useRef, useState, type CSSProperties } from "react";

/** Snackbar Material Design 3: aviso breve con acción "Deshacer", cierre automático que se pausa al apuntar o enfocar, y anuncio por región viva. React + TypeScript, sin dependencias. */
export type CapaSnackbarProps = {
  trigger?: string;
  message?: string;
  action?: string;
  duration?: number;
  onAction?: () => void;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gccts-root { box-sizing: border-box; display: grid; justify-items: center; gap: 28px; width: 100%; max-width: 480px; color: #1d1b20; font: 400 14px/20px Roboto, "Google Sans", ui-sans-serif, system-ui, sans-serif; letter-spacing: .25px; }
.gccts-root *, .gccts-root *::before, .gccts-root *::after { box-sizing: border-box; }
.gccts-root button { font: inherit; letter-spacing: inherit; }
.gccts-root :focus-visible { outline: 3px solid #d0bcff; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .gccts-bar { animation: none !important; } .gccts-snack { animation: none !important; } }
.gccts-open { height: 40px; padding: 0 24px; border: 0; border-radius: 20px; background: #6750a4; color: #fff; font-weight: 500; letter-spacing: .1px; cursor: pointer; transition: box-shadow .2s; }
.gccts-open:hover { box-shadow: 0 1px 3px 1px rgba(0, 0, 0, .15), 0 1px 2px rgba(0, 0, 0, .3); }
.gccts-slot { position: relative; width: 100%; height: 72px; }
.gccts-snack { position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 8px; min-height: 48px; padding: 0 8px 0 16px; border-radius: 4px; background: #322f35; color: #f5eff7; box-shadow: 0 1px 3px 1px rgba(0, 0, 0, .15), 0 1px 2px rgba(0, 0, 0, .3); overflow: hidden; animation: gccts-in .3s cubic-bezier(.2, 0, 0, 1); }
.gccts-msg { flex: 1; padding: 14px 0; }
.gccts-act { height: 40px; padding: 0 12px; border: 0; border-radius: 20px; background: none; color: #d0bcff; font-weight: 500; letter-spacing: .1px; cursor: pointer; }
.gccts-act:hover { background: rgba(208, 188, 255, .12); }
.gccts-x { display: grid; place-items: center; width: 40px; height: 40px; border: 0; border-radius: 50%; background: none; color: #e6e0e9; cursor: pointer; }
.gccts-x:hover { background: rgba(230, 224, 233, .12); }
.gccts-bar { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: #d0bcff; transform-origin: left; animation: gccts-run var(--d) linear forwards; animation-play-state: var(--ps, running); }
@keyframes gccts-in { from { opacity: 0; transform: translateY(16px) scale(.98); } }
@keyframes gccts-run { from { transform: scaleX(1); } to { transform: scaleX(0); } }`;

export function CapaSnackbar({ trigger = "Archivar conversación", message = "Conversación archivada", action = "Deshacer", duration = 6000, onAction, className = "", style }: CapaSnackbarProps) {
  const [show, setShow] = useState(false);
  const [paused, setPaused] = useState(false);
  const [key, setKey] = useState(0);
  const timer = useRef(0);
  const started = useRef(0);
  const left = useRef(duration);

  function clear() { window.clearTimeout(timer.current); }
  function arm(ms: number) {
    clear();
    started.current = Date.now();
    left.current = ms;
    timer.current = window.setTimeout(() => setShow(false), ms);
  }
  useEffect(() => clear, []);
  useEffect(() => {
    if (!show) return;
    const esc = (event: KeyboardEvent) => event.key === "Escape" && setShow(false);
    document.addEventListener("keydown", esc);
    return () => document.removeEventListener("keydown", esc);
  }, [show]);

  function open() {
    setPaused(false);
    setKey((value) => value + 1);
    setShow(true);
    arm(duration);
  }
  function pause() { if (!show) return; setPaused(true); left.current -= Date.now() - started.current; clear(); }
  function resume() { if (!show) return; setPaused(false); arm(Math.max(1200, left.current)); }

  return (
    <div className={`gccts-root ${className}`} style={style}>
      <style>{CSS}</style>
      <button className="gccts-open" type="button" onClick={open}>{trigger}</button>
      <div className="gccts-slot" role="status" aria-live="polite">
        {show ? (
          <div key={key} className="gccts-snack" onPointerEnter={pause} onPointerLeave={resume} onFocus={pause} onBlur={resume}>
            <span className="gccts-msg">{message}</span>
            <button className="gccts-act" type="button" onClick={() => { onAction?.(); setShow(false); }}>{action}</button>
            <button className="gccts-x" type="button" aria-label="Cerrar aviso" onClick={() => setShow(false)}>
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18" /></svg>
            </button>
            <span className="gccts-bar" style={{ "--d": `${duration}ms`, "--ps": paused ? "paused" : "running" } as CSSProperties} />
          </div>
        ) : null}
      </div>
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/capa-snackbar

Ejemplo de uso de Snackbar Material Design 3

Después de copiar capa-snackbar.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { CapaSnackbar } from "./capa-snackbar";

export default function Ejemplo() {
  return <CapaSnackbar trigger="Ejemplo" />;
}

Propiedades de CapaSnackbar

CapaSnackbar acepta 7 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
trigger?stringOpcional.
message?stringOpcional.
action?stringOpcional.
duration?numberOpcional.
onAction?() => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Snackbar Material Design 3 con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--psrunning

Accesibilidad y rendimiento

El archivo tiene 86 líneas (5 KB) y define 2 animaciones con @keyframes. Esto es lo que hace Snackbar Material Design 3 por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
  • Declara roles ARIA para lectores de pantalla: status.
  • Responde al teclado, no solo al ratón.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)