{
  "name": "toast",
  "title": {
    "es": "Aviso (toast)",
    "en": "Toast"
  },
  "summary": {
    "es": "Mensaje breve que se apila y se despliega al pasar el ratón.",
    "en": "Short message that stacks and spreads on hover."
  },
  "category": "feedback",
  "status": "stable",
  "nivel": "componentes",
  "updated": "2026-09-29",
  "license": "MIT",
  "page": "https://componentes-lovat.vercel.app/c/toast/",
  "requires": [
    "button"
  ],
  "files": [
    {
      "type": "html",
      "url": "https://componentes-lovat.vercel.app/raw/toast/toast.html",
      "content": "<div class=\"c-btn-row\">\n  <button class=\"c-btn c-btn--outline\" type=\"button\" data-c-toast=\"Cambios guardados\" data-c-toast-desc=\"Tu perfil está al día.\" data-c-toast-type=\"success\">Guardar</button>\n  <button class=\"c-btn c-btn--outline\" type=\"button\" data-c-toast=\"Enlace copiado\" data-c-toast-type=\"info\">Copiar enlace</button>\n  <button class=\"c-btn c-btn--outline\" type=\"button\" data-c-toast=\"No se pudo exportar\" data-c-toast-desc=\"Prueba con un formato más pequeño.\" data-c-toast-type=\"error\">Simular error</button>\n</div>\n\n<!-- Una vez por página, al final del <body>. Debe existir desde que carga. -->\n<section aria-label=\"Notificaciones\">\n  <ol class=\"c-toast-viewport\" data-position=\"bottom-right\" role=\"status\" aria-live=\"polite\" aria-relevant=\"additions\"></ol>\n</section>\n"
    },
    {
      "type": "css",
      "url": "https://componentes-lovat.vercel.app/raw/toast/toast.css",
      "content": "/* toast · Componentes (MIRÓ Labs). Diseño de coss ui (MIT, © coss.com, antes Origin UI).\n   Generado por tools/build_css.py desde toast.tw.json. No editar a mano. Necesita tokens.css. */\n.c-toast__content{pointer-events:auto}\n.c-toast{position:absolute}\n.c-toast-viewport{position:fixed;z-index:60}\n.c-toast{z-index:calc(9999 - var(--toast-index))}\n.c-toast-viewport{margin-inline:auto;display:flex}\n.c-toast__content{display:flex}\n.c-toast__main{display:flex}\n.c-toast__text{display:flex}\n.c-toast{height:var(--toast-calc-height)}\n.c-toast-viewport{width:calc(100% - var(--toast-inset)*2)}\n.c-toast{width:100%}\n.c-toast-viewport{max-width:calc(var(--spacing)*90)}\n.c-toast__text{flex-direction:column}\n.c-toast__content{align-items:center;justify-content:space-between}\n.c-toast__text{gap:calc(var(--spacing)*.5)}\n.c-toast__content{gap:calc(var(--spacing)*1.5)}\n.c-toast__main{gap:calc(var(--spacing)*2)}\n.c-toast__content{overflow:hidden}\n.c-toast{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;background-color:var(--popover)}\n@supports (color:color-mix(in lab, red, red)){\n.c-toast{background-color:color-mix(in srgb,var(--popover),var(--color-black)calc(1%*max(0,var(--toast-index,0))))}\n}\n.c-toast__content{padding-inline:calc(var(--spacing)*3.5);padding-block:calc(var(--spacing)*3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}\n.c-toast__title{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}\n.c-toast__description{color:var(--muted-foreground)}\n.c-toast{color:var(--popover-foreground);--tw-shadow-alpha:5%;--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000000d),0 4px 6px -4px var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}\n@supports (color:lab(0% 0 0)){\n.c-toast{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,lab(0% 0 0/.05)),0 4px 6px -4px var(--tw-shadow-color,lab(0% 0 0/.05))}\n}\n.c-toast__content{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.25s;transition-duration:.25s}\n.c-toast{-webkit-user-select:none;user-select:none;--toast-calc-height:var(--toast-frontmost-height,var(--toast-height));--toast-gap:calc(var(--spacing)*3)}\n.c-toast-viewport{--toast-inset:calc(var(--spacing)*4)}\n.c-toast{--toast-peek:calc(var(--spacing)*3);--toast-scale:calc(max(0,1 - (var(--toast-index)*.1)));--toast-shrink:calc(1 - var(--toast-scale));transition:transform .5s cubic-bezier(.22,1,.36,1),opacity .5s,height .15s,background-color .5s}\n.c-toast:not(:is(.dark *)){background-clip:padding-box}\n.c-toast:before{content:var(--tw-content);pointer-events:none;content:var(--tw-content);position:absolute;content:var(--tw-content);inset:calc(var(--spacing)*0);content:var(--tw-content);border-radius:calc(var(--radius-lg) - 1px);content:var(--tw-content);--tw-shadow:0 1px var(--tw-shadow-color,#0000000a)}\n@supports (color:color-mix(in lab, red, red)){\n.c-toast:before{--tw-shadow:0 1px var(--tw-shadow-color,color-mix(in oklab,var(--color-black)4%,transparent))}\n}\n.c-toast:before{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}\n.c-toast:after{content:var(--tw-content);position:absolute;content:var(--tw-content);left:calc(var(--spacing)*0);content:var(--tw-content);height:calc(var(--toast-gap) + 1px);content:var(--tw-content);width:100%}\n.c-toast__content[data-behind]{opacity:0}\n.c-toast__content[data-behind]:not([data-expanded]){pointer-events:none}\n.c-toast[data-expanded]{background-color:var(--popover)}\n.c-toast__content[data-expanded]{opacity:1}\n.c-toast[data-position][data-expanded]{transform:translateX(var(--toast-swipe-movement-x))translateY(var(--toast-calc-offset-y))}\n.c-toast[data-position*=bottom]{top:auto}\n.c-toast-viewport[data-position*=bottom]{bottom:var(--toast-inset)}\n.c-toast[data-position*=bottom]{bottom:calc(var(--spacing)*0);transform-origin:50% calc(50% + 50%*min(var(--toast-index,0),1));transform:translateX(var(--toast-swipe-movement-x))translateY(calc(var(--toast-swipe-movement-y) - (var(--toast-index)*var(--toast-peek)) - (var(--toast-shrink)*var(--toast-calc-height))))scale(var(--toast-scale));--toast-calc-offset-y:calc(var(--toast-offset-y)*-1 + var(--toast-index)*var(--toast-gap)*-1 + var(--toast-swipe-movement-y))}\n.c-toast[data-position*=bottom]:after{content:var(--tw-content);bottom:100%}\n.c-toast[data-position*=bottom][data-starting-style]{transform:translateY(calc(100% + var(--toast-inset)))}\n.c-toast[data-position*=bottom][data-ending-style]:not([data-limited]):not([data-swipe-direction]){transform:translateY(calc(100% + var(--toast-inset)))}\n.c-toast-viewport[data-position*=right]{right:var(--toast-inset)}\n.c-toast[data-position*=right]{right:calc(var(--spacing)*0);left:auto}\n@media (min-width:40rem){\n.c-toast-viewport{--toast-inset:calc(var(--spacing)*8)}\n}\n.c-toast:is(.dark *){background-color:var(--popover)}\n@supports (color:color-mix(in lab, red, red)){\n.c-toast:is(.dark *){background-color:color-mix(in srgb,var(--popover),var(--color-black)calc(6%*max(0,var(--toast-index,0))))}\n}\n.c-toast:is(.dark *):before{content:var(--tw-content);--tw-shadow:0 -1px var(--tw-shadow-color,#ffffff0f)}\n@supports (color:color-mix(in lab, red, red)){\n.c-toast:is(.dark *):before{--tw-shadow:0 -1px var(--tw-shadow-color,color-mix(in oklab,var(--color-white)6%,transparent))}\n}\n.c-toast:is(.dark *):before{box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}\n.c-toast:is(.dark *)[data-expanded]{background-color:var(--popover)}\n.c-toast__content svg{pointer-events:none;flex-shrink:0}\n.c-toast__content>svg{height:1lh;width:calc(var(--spacing)*4)}\n/* propio */\n.c-toast-viewport{list-style:none;margin:0;padding:0}\n.c-toast{list-style:none}\n.c-toast[data-starting-style],.c-toast[data-ending-style]{opacity:0}\n.c-toast__icon{flex:none;width:1rem;height:1lh}\n.c-toast[data-type=success] .c-toast__icon{color:var(--success)}\n.c-toast[data-type=error] .c-toast__icon{color:var(--destructive)}\n.c-toast[data-type=info] .c-toast__icon{color:var(--info)}\n.c-toast[data-type=warning] .c-toast__icon{color:var(--warning)}\n"
    },
    {
      "type": "js",
      "url": "https://componentes-lovat.vercel.app/raw/toast/toast.js",
      "content": "/* Aviso (toast) con la pila de coss ui: el más nuevo delante, los demás asoman detrás y se despliegan\n   al pasar el ratón o enfocar. Máximo 3 a la vista, 5 s cada uno, pausa con ratón, foco o pestaña oculta.\n   Uso: Componentes.toast('Título', { description: 'Detalle', type: 'success' | 'error' | 'info' | 'warning' }) */\n(function () {\n  const C = (window.Componentes = window.Componentes || {});\n  const CFG = { duration: 5000, limit: 3, leaveMs: 500 };\n  const ICONS = {\n    success: '<path d=\"M21.801 10A10 10 0 1 1 17 3.335\"/><path d=\"m9 11 3 3L22 4\"/>',\n    error: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6\"/><path d=\"m9 9 6 6\"/>',\n    info: '<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/>',\n    warning: '<path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/>',\n  };\n  let vp; const live = new Map();\n\n  function viewport() {\n    if (vp && document.contains(vp)) return vp;\n    vp = document.querySelector('.c-toast-viewport');\n    if (!vp) { console.warn('[toast] falta .c-toast-viewport'); return null; }\n    const expand = (on) => {\n      if (on) vp.setAttribute('data-expanded', ''); else vp.removeAttribute('data-expanded');\n      layout();\n      live.forEach((t, li) => (on ? pause(li) : resume(li)));\n    };\n    vp.addEventListener('pointerover', () => expand(true));\n    vp.addEventListener('pointerleave', () => { if (!vp.contains(document.activeElement)) expand(false); });\n    vp.addEventListener('focusin', () => expand(true));\n    vp.addEventListener('focusout', (e) => { if (!vp.contains(e.relatedTarget)) expand(false); });\n    document.addEventListener('visibilitychange', () => live.forEach((t, li) => (document.hidden ? pause(li) : resume(li))));\n    return vp;\n  }\n\n  function layout() {\n    const list = [...vp.children].filter((li) => !li.hasAttribute('data-ending-style')).reverse();\n    const expanded = vp.hasAttribute('data-expanded');\n    const front = list[0] ? list[0].offsetHeight : 0;\n    let offset = 0;\n    list.forEach((li, i) => {\n      li.style.setProperty('--toast-index', i);\n      li.style.setProperty('--toast-offset-y', offset + 'px');\n      if (expanded) li.style.removeProperty('--toast-frontmost-height');\n      else li.style.setProperty('--toast-frontmost-height', front + 'px');\n      offset += li.querySelector('.c-toast__content').offsetHeight;\n      li.toggleAttribute('data-behind', i > 0);\n      li.toggleAttribute('data-limited', i >= CFG.limit);\n      li.toggleAttribute('data-expanded', expanded);\n    });\n  }\n\n  function pause(li) { const t = live.get(li); if (t && t.timer) { clearTimeout(t.timer); t.timer = 0; t.left -= Date.now() - t.start; } }\n  function resume(li) {\n    const t = live.get(li);\n    if (!t || t.timer || document.hidden || vp.hasAttribute('data-expanded')) return;\n    t.start = Date.now(); t.timer = setTimeout(() => dismiss(li), Math.max(800, t.left));\n  }\n\n  function dismiss(li) {\n    pause(li); live.delete(li);\n    li.setAttribute('data-ending-style', '');\n    layout();\n    setTimeout(() => { li.remove(); layout(); }, CFG.leaveMs);\n  }\n\n  function toast(title, opts) {\n    try {\n      opts = opts || {};\n      if (!viewport()) return null;\n      const li = document.createElement('li');\n      li.className = 'c-toast';\n      li.dataset.position = vp.dataset.position || 'bottom-right';\n      if (opts.type) li.dataset.type = opts.type;\n      li.style.setProperty('--toast-swipe-movement-x', '0px');\n      li.style.setProperty('--toast-swipe-movement-y', '0px');\n      const icon = ICONS[opts.type] ? `<svg class=\"c-toast__icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">${ICONS[opts.type]}</svg>` : '';\n      li.innerHTML = `<div class=\"c-toast__content\"><div class=\"c-toast__main\">${icon}<div class=\"c-toast__text\"><p class=\"c-toast__title\"></p>${opts.description ? '<p class=\"c-toast__description\"></p>' : ''}</div></div></div>`;\n      li.querySelector('.c-toast__title').textContent = String(title);\n      if (opts.description) li.querySelector('.c-toast__description').textContent = String(opts.description);\n      li.setAttribute('data-starting-style', '');\n      vp.append(li);\n      li.style.setProperty('--toast-height', li.querySelector('.c-toast__content').offsetHeight + 'px');\n      layout();\n      requestAnimationFrame(() => requestAnimationFrame(() => li.removeAttribute('data-starting-style')));\n      live.set(li, { left: opts.duration || CFG.duration, start: 0, timer: 0 });\n      const extra = [...vp.children].filter((x) => live.has(x)).reverse().slice(CFG.limit);\n      extra.forEach(dismiss);\n      resume(li);\n      return li;\n    } catch (e) { console.warn('[toast]', e); return null; }\n  }\n\n  document.addEventListener('click', (e) => {\n    const b = e.target.closest?.('[data-c-toast]');\n    if (b) toast(b.dataset.cToast, { description: b.dataset.cToastDesc, type: b.dataset.cToastType });\n  });\n\n  C.toast = toast;\n})();\n"
    },
    {
      "type": "standalone",
      "url": "https://componentes-lovat.vercel.app/r/toast.html"
    }
  ],
  "tokensFile": "https://componentes-lovat.vercel.app/tokens.css",
  "darkTheme": "add class 'dark' to <html>",
  "contexto": "ambos",
  "densidad": "media",
  "tono": "sobria",
  "states": [
    "enter",
    "stacked",
    "expanded",
    "leave"
  ],
  "use": {
    "es": [
      "Para confirmar algo que ya pasó."
    ],
    "en": [
      "To confirm something that already happened."
    ]
  },
  "avoid": {
    "es": [
      "Para errores de formulario: ponlos junto al campo."
    ],
    "en": [
      "For form errors: place them next to the field."
    ]
  },
  "a11y": {
    "roles": [
      "status"
    ],
    "keyboard": [],
    "notes": {
      "es": [
        "La región role=status existe desde que carga la página.",
        "Máximo 3 a la vista, 5 s cada uno; se pausa con ratón, foco o pestaña oculta."
      ],
      "en": [
        "The role=status region exists from page load.",
        "At most 3 visible, 5 s each; pauses on hover, focus or hidden tab."
      ]
    },
    "test": {
      "es": [
        "Lanza cuatro seguidos: quedan tres."
      ],
      "en": [
        "Fire four in a row: three remain."
      ]
    }
  },
  "issues": {
    "es": [],
    "en": []
  },
  "credits": [
    {
      "source": "coss ui (MIT) · toast.tsx",
      "url": "https://coss.com/ui"
    },
    {
      "source": "Emil Kowalski, Building a toast component",
      "url": "https://emilkowal.ski/ui/building-a-toast-component"
    }
  ],
  "changes": [
    {
      "date": "2026-09-29",
      "es": "Diseño de coss ui en HTML, CSS y JS sin dependencias",
      "en": "coss ui design in dependency-free HTML, CSS and JS"
    }
  ],
  "partes": [],
  "pieza": "https://componentes-lovat.vercel.app/r/toast.html"
}
