20 CSS Ribbons18 / 20

Light JSMIT licensed

Slide-In Ribbon Notification

The announcement ribbon that drops from the top of the viewport — built on the platform's own popover API, so it lives in the top layer above everything without a z-index war. The entry animation uses @starting-style and transition-behavior: allow-discrete, the pair that finally makes it possible to animate an element in and out of display: none with pure CSS.

Published

Live Demo
Try it

The code

<section class="rib-18" aria-label="Slide in ribbon notification toast demo">
  <div class="rib-18__stage">
    <div class="rib-18__panel">
      <h2 class="rib-18__title">Ribbon notifications</h2>
      <p class="rib-18__sub">Three tones, one component. Each drops from the top layer with a <code>@starting-style</code> entry and an <code>allow-discrete</code> exit.</p>
      <div class="rib-18__buttons">
        <button class="rib-18__btn" type="button" popovertarget="rib-18-ok">Show success</button>
        <button class="rib-18__btn" type="button" popovertarget="rib-18-warn">Show warning</button>
        <button class="rib-18__btn" type="button" popovertarget="rib-18-err">Show error</button>
      </div>
      <p class="rib-18__note">Toasts pause their auto-dismiss while hovered or focused.</p>
    </div>

    <div class="rib-18__toast" id="rib-18-ok" popover="manual" data-tone="ok" role="status" data-timeout="4500">
      <span class="rib-18__ic" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="m9.6 16.4-4-4L4.2 13.8l5.4 5.4 10-10-1.4-1.4z"/></svg></span>
      <span class="rib-18__txt"><strong>Deployment complete.</strong> build 2481 is live in eu-west-1.</span>
      <button class="rib-18__x" type="button" popovertarget="rib-18-ok" popovertargetaction="hide" aria-label="Dismiss notification">✕</button>
    </div>
    <div class="rib-18__toast" id="rib-18-warn" popover="manual" data-tone="warn" role="status" data-timeout="6000">
      <span class="rib-18__ic" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 2 1 21h22L12 2Zm1 14h-2v2h2v-2Zm0-7h-2v5h2V9Z"/></svg></span>
      <span class="rib-18__txt"><strong>Certificate expires in 6 days.</strong> renew before 15 Aug to avoid downtime.</span>
      <button class="rib-18__x" type="button" popovertarget="rib-18-warn" popovertargetaction="hide" aria-label="Dismiss notification">✕</button>
    </div>
    <div class="rib-18__toast" id="rib-18-err" popover="manual" data-tone="err" role="alert" data-timeout="0">
      <span class="rib-18__ic" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 15h-2v-2h2v2Zm0-4h-2V7h2v6Z"/></svg></span>
      <span class="rib-18__txt"><strong>Payment method declined.</strong> update your card to keep the workspace active.</span>
      <button class="rib-18__x" type="button" popovertarget="rib-18-err" popovertargetaction="hide" aria-label="Dismiss notification">✕</button>
    </div>

    <p class="rib-18__chip" aria-hidden="true">popover=&quot;manual&quot; · @starting-style · transition-behavior: allow-discrete · overlay</p>
  </div>
</section>
.rib-18,
.rib-18 *,
.rib-18 *::before,
.rib-18 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.rib-18 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --rib-18-bg: #0e1014;
  --rib-18-bg: oklch(0.18 0.008 260);
  --rib-18-panel: #171a21;
  --rib-18-panel: oklch(0.23 0.01 260);
  --rib-18-ink: #eff1f6;
  --rib-18-ink: oklch(0.95 0.005 260);
  --rib-18-mut: #8a90a0;
  --rib-18-mut: oklch(0.65 0.014 260);
  --rib-18-ok: #2fbf71;
  --rib-18-ok: oklch(0.72 0.16 155);
  --rib-18-warn: #e8a83a;
  --rib-18-warn: oklch(0.78 0.15 78);
  --rib-18-err: #f2555f;
  --rib-18-err: oklch(0.66 0.21 18);
  background: var(--rib-18-bg);
  color: var(--rib-18-ink);
  font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  -webkit-font-smoothing: antialiased;
}

.rib-18__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: clamp(16px,3vw,24px);
  padding: clamp(20px,5vw,56px);
}

.rib-18__panel {
  width: min(100%,540px);
  display: grid;
  gap: 12px;
  padding: clamp(24px,5vw,34px);
  border-radius: 22px;
  background: var(--rib-18-panel);
  border: 1px solid oklch(1 0 0/.09);
  box-shadow: 0 30px 66px -34px oklch(0 0 0/.85);
}

.rib-18__title {
  font-size: clamp(22px,4vw,30px);
  font-weight: 660;
  letter-spacing: -.032em;
}

.rib-18__sub {
  font-size: 14px;
  line-height: 1.6;
  color: var(--rib-18-mut);
  text-wrap: pretty;
}

.rib-18__sub code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12.5px;
  padding: 2px 5px;
  border-radius: 5px;
  background: oklch(1 0 0/.08);
}

.rib-18__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
  margin-block-start: 6px;
}

.rib-18__btn {
  min-height: 46px;
  padding-inline: 18px;
  border: 1.5px solid oklch(1 0 0/.16);
  border-radius: 12px;
  background: oklch(1 0 0/.04);
  color: var(--rib-18-ink);
  font: inherit;
  font-size: 14px;
  font-weight: 620;
  cursor: pointer;
  transition: background .22s,border-color .22s,translate .18s;
}

.rib-18__btn:hover {
  background: oklch(1 0 0/.09);
  translate: 0 -1px;
}

.rib-18__btn:focus-visible {
  outline: 3px solid var(--rib-18-ok);
  outline-offset: 3px;
}

.rib-18__note {
  font-size: 12px;
  color: var(--rib-18-mut);
}

.rib-18__toast {
  position: fixed;
  inset-block-start: 22px;
  inset-inline: 0;
  margin-inline: auto;
  inline-size: min(calc(100% - 32px),520px);
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 14px 14px 14px 16px;
  border: 0;
  border-radius: 14px;
  color: oklch(0.16 0.01 260);
  background: var(--rib-18-tone,var(--rib-18-ok));
  box-shadow: 0 18px 44px -14px oklch(0 0 0/.7),inset 0 1px 0 oklch(1 0 0/.35);
  translate: 0 -140%;
  opacity: 0;
  transition: translate .5s cubic-bezier(.16,1,.3,1),opacity .32s ease,display .5s allow-discrete,overlay .5s allow-discrete;
}

.rib-18__toast[data-tone="ok"] {
  --rib-18-tone: var(--rib-18-ok);
}

.rib-18__toast[data-tone="warn"] {
  --rib-18-tone: var(--rib-18-warn);
}

.rib-18__toast[data-tone="err"] {
  --rib-18-tone: var(--rib-18-err);
}

.rib-18__toast:popover-open {
  translate: 0 0;
  opacity: 1;
}

@starting-style {
  .rib-18__toast:popover-open {
    translate: 0 -140%;
    opacity: 0;
  }
}

.rib-18__toast::backdrop {
  background: transparent;
}

.rib-18__ic {
  flex: none;
  inline-size: 30px;
  block-size: 30px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  background: oklch(0 0 0/.16);
}

.rib-18__txt {
  flex: 1;
  font-size: 13.5px;
  line-height: 1.42;
  text-wrap: pretty;
}

.rib-18__txt strong {
  font-weight: 750;
}

.rib-18__x {
  flex: none;
  inline-size: 34px;
  block-size: 34px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 9px;
  background: oklch(0 0 0/.1);
  color: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background .2s;
}

.rib-18__x:hover {
  background: oklch(0 0 0/.22);
}

.rib-18__x:focus-visible {
  outline: 2.5px solid oklch(0.16 0.01 260);
  outline-offset: 2px;
}

.rib-18__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  color: var(--rib-18-mut);
  padding: 8px 15px;
  border: 1px solid oklch(1 0 0/.14);
  border-radius: 99px;
  text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .rib-18__toast {
    transition: opacity .01ms,display .01ms allow-discrete,overlay .01ms allow-discrete;
    translate: 0 0;
  }

  @starting-style {
    .rib-18__toast:popover-open {
      translate: 0 0;
      opacity: 0;
    }
  }
}
(() => {
  document.querySelectorAll('.rib-18__stage').forEach((stage) => {
    if (stage.dataset.wired) return;
    stage.dataset.wired = '1';

    stage.querySelectorAll('.rib-18__toast').forEach((toast) => {
      const ms = +toast.dataset.timeout || 0;   // 0 = never auto-dismiss (errors)
      let timer = 0, remaining = ms, startedAt = 0;

      const start = () => {
        if (!remaining) return;
        startedAt = performance.now();
        timer = setTimeout(() => toast.hidePopover(), remaining);
      };
      const pause = () => {                      // WCAG 2.2.1: never yank it away mid-read
        if (!timer) return;
        clearTimeout(timer); timer = 0;
        remaining -= performance.now() - startedAt;
      };

      toast.addEventListener('toggle', (e) => {
        if (e.newState === 'open') { remaining = ms; start(); }
        else { clearTimeout(timer); timer = 0; }
      });
      toast.addEventListener('pointerenter', pause);
      toast.addEventListener('focusin', pause);
      toast.addEventListener('pointerleave', start);
      toast.addEventListener('focusout', (e) => { if (!toast.contains(e.relatedTarget)) start(); });
    });

    // Only one ribbon at a time — manual popovers do not close each other.
    stage.querySelectorAll('.rib-18__btn').forEach((btn) => {
      btn.addEventListener('click', () => {
        stage.querySelectorAll('.rib-18__toast').forEach((t) => {
          if (t.id !== btn.getAttribute('popovertarget') && t.matches(':popover-open')) t.hidePopover();
        });
      });
    });
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Ribbon from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Slide-In Ribbon Notification
Source: https://codefronts.com/snippets/css-ribbons/slide-in-ribbon-notification/

The announcement ribbon that drops from the top of the viewport — built on the platform's own popover API, so it lives in the top layer above everything without a z-index war. The entry animation uses @starting-style and transition-behavior: allow-discrete, the pair that finally makes it possible to animate an element in and out of display: none with pure CSS.
## HTML
```html
<section class="rib-18" aria-label="Slide in ribbon notification toast demo">
  <div class="rib-18__stage">
    <div class="rib-18__panel">
      <h2 class="rib-18__title">Ribbon notifications</h2>
      <p class="rib-18__sub">Three tones, one component. Each drops from the top layer with a <code>@starting-style</code> entry and an <code>allow-discrete</code> exit.</p>
      <div class="rib-18__buttons">
        <button class="rib-18__btn" type="button" popovertarget="rib-18-ok">Show success</button>
        <button class="rib-18__btn" type="button" popovertarget="rib-18-warn">Show warning</button>
        <button class="rib-18__btn" type="button" popovertarget="rib-18-err">Show error</button>
      </div>
      <p class="rib-18__note">Toasts pause their auto-dismiss while hovered or focused.</p>
    </div>

    <div class="rib-18__toast" id="rib-18-ok" popover="manual" data-tone="ok" role="status" data-timeout="4500">
      <span class="rib-18__ic" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="m9.6 16.4-4-4L4.2 13.8l5.4 5.4 10-10-1.4-1.4z"/></svg></span>
      <span class="rib-18__txt"><strong>Deployment complete.</strong> build 2481 is live in eu-west-1.</span>
      <button class="rib-18__x" type="button" popovertarget="rib-18-ok" popovertargetaction="hide" aria-label="Dismiss notification">✕</button>
    </div>
    <div class="rib-18__toast" id="rib-18-warn" popover="manual" data-tone="warn" role="status" data-timeout="6000">
      <span class="rib-18__ic" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 2 1 21h22L12 2Zm1 14h-2v2h2v-2Zm0-7h-2v5h2V9Z"/></svg></span>
      <span class="rib-18__txt"><strong>Certificate expires in 6 days.</strong> renew before 15 Aug to avoid downtime.</span>
      <button class="rib-18__x" type="button" popovertarget="rib-18-warn" popovertargetaction="hide" aria-label="Dismiss notification">✕</button>
    </div>
    <div class="rib-18__toast" id="rib-18-err" popover="manual" data-tone="err" role="alert" data-timeout="0">
      <span class="rib-18__ic" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 15h-2v-2h2v2Zm0-4h-2V7h2v6Z"/></svg></span>
      <span class="rib-18__txt"><strong>Payment method declined.</strong> update your card to keep the workspace active.</span>
      <button class="rib-18__x" type="button" popovertarget="rib-18-err" popovertargetaction="hide" aria-label="Dismiss notification">✕</button>
    </div>

    <p class="rib-18__chip" aria-hidden="true">popover=&quot;manual&quot; · @starting-style · transition-behavior: allow-discrete · overlay</p>
  </div>
</section>
```
## CSS
```css
.rib-18,
.rib-18 *,
.rib-18 *::before,
.rib-18 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.rib-18 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --rib-18-bg: #0e1014;
  --rib-18-bg: oklch(0.18 0.008 260);
  --rib-18-panel: #171a21;
  --rib-18-panel: oklch(0.23 0.01 260);
  --rib-18-ink: #eff1f6;
  --rib-18-ink: oklch(0.95 0.005 260);
  --rib-18-mut: #8a90a0;
  --rib-18-mut: oklch(0.65 0.014 260);
  --rib-18-ok: #2fbf71;
  --rib-18-ok: oklch(0.72 0.16 155);
  --rib-18-warn: #e8a83a;
  --rib-18-warn: oklch(0.78 0.15 78);
  --rib-18-err: #f2555f;
  --rib-18-err: oklch(0.66 0.21 18);
  background: var(--rib-18-bg);
  color: var(--rib-18-ink);
  font-family: system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  -webkit-font-smoothing: antialiased;
}

.rib-18__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: clamp(16px,3vw,24px);
  padding: clamp(20px,5vw,56px);
}

.rib-18__panel {
  width: min(100%,540px);
  display: grid;
  gap: 12px;
  padding: clamp(24px,5vw,34px);
  border-radius: 22px;
  background: var(--rib-18-panel);
  border: 1px solid oklch(1 0 0/.09);
  box-shadow: 0 30px 66px -34px oklch(0 0 0/.85);
}

.rib-18__title {
  font-size: clamp(22px,4vw,30px);
  font-weight: 660;
  letter-spacing: -.032em;
}

.rib-18__sub {
  font-size: 14px;
  line-height: 1.6;
  color: var(--rib-18-mut);
  text-wrap: pretty;
}

.rib-18__sub code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 12.5px;
  padding: 2px 5px;
  border-radius: 5px;
  background: oklch(1 0 0/.08);
}

.rib-18__buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 9px;
  margin-block-start: 6px;
}

.rib-18__btn {
  min-height: 46px;
  padding-inline: 18px;
  border: 1.5px solid oklch(1 0 0/.16);
  border-radius: 12px;
  background: oklch(1 0 0/.04);
  color: var(--rib-18-ink);
  font: inherit;
  font-size: 14px;
  font-weight: 620;
  cursor: pointer;
  transition: background .22s,border-color .22s,translate .18s;
}

.rib-18__btn:hover {
  background: oklch(1 0 0/.09);
  translate: 0 -1px;
}

.rib-18__btn:focus-visible {
  outline: 3px solid var(--rib-18-ok);
  outline-offset: 3px;
}

.rib-18__note {
  font-size: 12px;
  color: var(--rib-18-mut);
}

.rib-18__toast {
  position: fixed;
  inset-block-start: 22px;
  inset-inline: 0;
  margin-inline: auto;
  inline-size: min(calc(100% - 32px),520px);
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 14px 14px 14px 16px;
  border: 0;
  border-radius: 14px;
  color: oklch(0.16 0.01 260);
  background: var(--rib-18-tone,var(--rib-18-ok));
  box-shadow: 0 18px 44px -14px oklch(0 0 0/.7),inset 0 1px 0 oklch(1 0 0/.35);
  translate: 0 -140%;
  opacity: 0;
  transition: translate .5s cubic-bezier(.16,1,.3,1),opacity .32s ease,display .5s allow-discrete,overlay .5s allow-discrete;
}

.rib-18__toast[data-tone="ok"] {
  --rib-18-tone: var(--rib-18-ok);
}

.rib-18__toast[data-tone="warn"] {
  --rib-18-tone: var(--rib-18-warn);
}

.rib-18__toast[data-tone="err"] {
  --rib-18-tone: var(--rib-18-err);
}

.rib-18__toast:popover-open {
  translate: 0 0;
  opacity: 1;
}

@starting-style {
  .rib-18__toast:popover-open {
    translate: 0 -140%;
    opacity: 0;
  }
}

.rib-18__toast::backdrop {
  background: transparent;
}

.rib-18__ic {
  flex: none;
  inline-size: 30px;
  block-size: 30px;
  display: grid;
  place-items: center;
  border-radius: 9px;
  background: oklch(0 0 0/.16);
}

.rib-18__txt {
  flex: 1;
  font-size: 13.5px;
  line-height: 1.42;
  text-wrap: pretty;
}

.rib-18__txt strong {
  font-weight: 750;
}

.rib-18__x {
  flex: none;
  inline-size: 34px;
  block-size: 34px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 9px;
  background: oklch(0 0 0/.1);
  color: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: background .2s;
}

.rib-18__x:hover {
  background: oklch(0 0 0/.22);
}

.rib-18__x:focus-visible {
  outline: 2.5px solid oklch(0.16 0.01 260);
  outline-offset: 2px;
}

.rib-18__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  color: var(--rib-18-mut);
  padding: 8px 15px;
  border: 1px solid oklch(1 0 0/.14);
  border-radius: 99px;
  text-align: center;
}

@media (prefers-reduced-motion: reduce) {
  .rib-18__toast {
    transition: opacity .01ms,display .01ms allow-discrete,overlay .01ms allow-discrete;
    translate: 0 0;
  }

  @starting-style {
    .rib-18__toast:popover-open {
      translate: 0 0;
      opacity: 0;
    }
  }
}
```

## JavaScript
```js
(() => {
  document.querySelectorAll('.rib-18__stage').forEach((stage) => {
    if (stage.dataset.wired) return;
    stage.dataset.wired = '1';

    stage.querySelectorAll('.rib-18__toast').forEach((toast) => {
      const ms = +toast.dataset.timeout || 0;   // 0 = never auto-dismiss (errors)
      let timer = 0, remaining = ms, startedAt = 0;

      const start = () => {
        if (!remaining) return;
        startedAt = performance.now();
        timer = setTimeout(() => toast.hidePopover(), remaining);
      };
      const pause = () => {                      // WCAG 2.2.1: never yank it away mid-read
        if (!timer) return;
        clearTimeout(timer); timer = 0;
        remaining -= performance.now() - startedAt;
      };

      toast.addEventListener('toggle', (e) => {
        if (e.newState === 'open') { remaining = ms; start(); }
        else { clearTimeout(timer); timer = 0; }
      });
      toast.addEventListener('pointerenter', pause);
      toast.addEventListener('focusin', pause);
      toast.addEventListener('pointerleave', start);
      toast.addEventListener('focusout', (e) => { if (!toast.contains(e.relatedTarget)) start(); });
    });

    // Only one ribbon at a time — manual popovers do not close each other.
    stage.querySelectorAll('.rib-18__btn').forEach((btn) => {
      btn.addEventListener('click', () => {
        stage.querySelectorAll('.rib-18__toast').forEach((t) => {
          if (t.id !== btn.getAttribute('popovertarget') && t.matches(':popover-open')) t.hidePopover();
        });
      });
    });
  });
})();
```

How this works

Animating something that starts as display: none was impossible for twenty years. Three modern features solve it together:

.toast {
  transition: translate .5s cubic-bezier(.16,1,.3,1),
              opacity .32s ease,
              display .5s allow-discrete,    /* keep it rendered while leaving */
              overlay .5s allow-discrete;    /* keep it in the TOP LAYER too  */
  translate: 0 -140%; opacity: 0;
}
.toast:popover-open { translate: 0 0; opacity: 1; }

@starting-style {                            /* the value to animate FROM */
  .toast:popover-open { translate: 0 -140%; opacity: 0; }
}

@starting-style supplies the "before" frame for an element that has just been rendered for the first time; allow-discrete on display and overlay defers those discrete flips to the end of the transition, so the exit animation actually gets to play instead of the element vanishing instantly.

<button popovertarget="toast-1">Show</button>
<div id="toast-1" popover="manual" role="status">…</div>

The popovertarget attribute wires the button to the popover with zero JavaScript — the platform handles the top layer, the light-dismiss behaviour (for popover="auto"), and Escape-to-close. manual is used here so a toast is not dismissed by a stray click elsewhere.

The only script is the auto-dismiss timer, and it is written carefully: the timer pauses on hover and on focus, because a notification that disappears while a user is reading it — or while a screen reader is announcing it — is a bug. role="status" announces politely; use role="alert" only for genuine errors, since it interrupts.

Make it yours

  • Change position by flipping the anchor: inset-block-end: 24px plus translate: 0 120% for a bottom toast; the whole transition set is unchanged.
  • Variant colours from one attribute: [data-tone="warn"] / "error" / "ok" each set two custom properties and nothing else.
  • Stack multiple toasts by putting them in a position: fixed flex column and giving each its own popover — the top layer preserves their order.
  • Full-bleed announcement bar instead of a floating pill: set inline-size: 100%, border-radius: 0 and drop the shadow.

Gotchas — read before shipping

  • Without transition-behavior: allow-discrete on display, the exit animation never plays — the element is removed on the first frame.
  • Forgetting overlay in the transition list makes a popover leave the top layer immediately, so it can be painted behind other content while animating out.
  • @starting-style rules must come after the rule they modify, or the cascade will not apply them.
  • Auto-dismiss without a pause-on-hover/focus is an accessibility failure (WCAG 2.2.1). Give users at least 5 seconds or a way to stop the timer.
  • role="alert" interrupts screen-reader output immediately. Use role="status" for anything that is not urgent, or you make the interface hostile.

Browser support

ChromeSafariFirefoxEdge
117+17.4+129+117+

The popover API is Baseline since Firefox 125 (April 2024); @starting-style and transition-behavior: allow-discrete are Baseline since Firefox 129 (Aug 2024). In an older engine the toast appears and disappears instantly — no animation, full functionality.

Techniques used in this demo

Search CodeFronts

Loading…