Interaction blueprint

Signal toast

Confirmation enters from the same area the action occurred, then departs before it interrupts the next thought.

Playground

Playhead 0ms / 2400ms

Build it

Choose a target and adapt the source to your interface.

189 lines
index.html
<div class="signal-demo">
  <button class="publish-button" id="publish" type="button">
    <svg viewBox="0 0 20 20" aria-hidden="true">
      <path d="M10 13V3m-4 4 4-4 4 4M4 12v5h12v-5" /></svg
    >Publish project
  </button>
  <div
    class="signal-toast"
    role="status"
    aria-live="polite"
    aria-atomic="true"
    hidden
  >
    <span class="toast-check" aria-hidden="true"
      ><svg viewBox="0 0 20 20" aria-hidden="true">
        <path d="m5 10 3 3 7-7" /></svg></span
    ><span
      ><strong>Project is live</strong
      ><small>Your changes are ready to share.</small></span
    >
  </div>
</div>

<script>
  const trigger = document.querySelector('#publish');
  const toast = document.querySelector('.signal-toast');
  trigger.addEventListener('click', () => {
    toast.classList.remove('is-running');
    toast.hidden = false;
    void toast.offsetWidth;
    toast.classList.add('is-running');
  });
  toast.addEventListener('animationend', (event) => {
    if (event.target !== toast) return;
    toast.hidden = true;
    toast.classList.remove('is-running');
  });
</script>

<style>
  .signal-demo {
    --toast-bg: var(--stage-bg, var(--bg, #f8f9fb));
    --toast-ink: var(--stage-ink, var(--ink, #252b34));
    --toast-muted: var(--stage-muted, var(--muted, #667080));
    --toast-line: var(--stage-grid, var(--line, #dce1e8));
    --toast-accent: var(--stage-link, var(--brand-deep, #175bc5));
    position: relative;
    width: 320px;
    max-width: 100%;
    height: 146px;
    padding-top: 8px;
    color: var(--toast-ink);
    font:
      14px/1.5 system-ui,
      sans-serif;
    text-align: center;
  }
  .publish-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    min-height: 44px;
    padding: 0 16px;
    border: 1px solid var(--toast-line);
    border-radius: 8px;
    background: var(--toast-ink);
    color: var(--toast-bg);
    font:
      500 13px system-ui,
      sans-serif;
    cursor: pointer;
  }
  .publish-button svg {
    width: 16px;
    height: 16px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .publish-button:hover {
    opacity: 0.88;
  }
  .publish-button:focus-visible {
    outline: 2px solid var(--toast-accent);
    outline-offset: 4px;
  }
  .signal-toast {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    border: 1px solid var(--toast-line);
    border-radius: 12px;
    background: color-mix(in srgb, var(--toast-bg), var(--toast-ink) 3%);
    color: var(--toast-ink);
    box-shadow: 0 4px 16px #00000008;
    text-align: left;
  }
  .signal-toast[hidden] {
    display: none;
  }
  .toast-check {
    display: grid;
    place-items: center;
    flex: none;
    width: 32px;
    height: 32px;
    border: 1px solid color-mix(in srgb, var(--toast-accent) 30%, transparent);
    border-radius: 50%;
    color: var(--toast-accent);
    background: color-mix(in srgb, var(--toast-accent) 8%, transparent);
  }
  .toast-check svg {
    width: 17px;
    height: 17px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
  }
  .signal-toast strong {
    display: block;
    font-size: 13px;
    font-weight: 600;
  }
  .signal-toast small {
    display: block;
    margin-top: 2px;
    font-size: 11px;
    color: var(--toast-muted);
  }
  .signal-toast.is-running {
    animation: signal-toast 2400ms linear both;
  }
  @keyframes signal-toast {
    0% {
      opacity: 0;
      transform: translateY(16px);
      animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
    }
    8.3333% {
      opacity: 1;
      transform: translateY(0);
      animation-timing-function: linear;
    }
    83.3333% {
      opacity: 1;
      transform: translateY(0);
      animation-timing-function: cubic-bezier(0.4, 0, 1, 1);
    }
    100% {
      opacity: 0;
      transform: translateY(12px);
    }
  }
  @media (prefers-color-scheme: dark) {
    .signal-demo {
      --toast-bg: var(--stage-bg, var(--bg, #101317));
      --toast-ink: var(--stage-ink, var(--ink, #eef0f3));
      --toast-muted: var(--stage-muted, var(--muted, #a8b1be));
      --toast-line: var(--stage-grid, var(--line, #353d49));
      --toast-accent: var(--stage-link, var(--brand-deep, #94bfff));
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .signal-toast.is-running {
      animation: signal-toast-reduced 2400ms steps(1, end) both;
    }
    @keyframes signal-toast-reduced {
      0%,
      99.99% {
        opacity: 1;
        transform: none;
      }
      100% {
        opacity: 0;
        transform: none;
      }
    }
  }
</style>

Animation breakdown

Frame phases, motion intent, implementation constraints and accessible alternatives.

2.4s
01

0–200ms: toast rises into view.

02

200–2000ms: it remains readable.

03

2000–2400ms: it departs without blocking work.

Motion intent

Confirmation enters from the same area the action occurred, then departs before it interrupts the next thought.

The movement covers multiple visual phases, so the extra time preserves the relationship between them.

The fast acceleration and long ease-out make the state feel responsive while giving the final pixels time to settle.

Motion specification

Trigger
A project publish succeeds.
Timing
2.4s
Easing
cubic-bezier(.22, 1, .36, 1)

Animated properties

transformopacity

Implementation notes

  • Keep hidden and visible aligned to the same visual anchor.
  • Drive the transition from semantic state, not a timer, when the trigger is interactive.
  • Use transform, opacity and avoid layout work inside the animation loop.

Reduced motion

Swap to the final state instantly, preserving colour, label, and accessible state.

  • Use a minimum 44 by 44 pixel interactive target.
  • Expose the hidden and visible states with aria-pressed, aria-expanded, or an updated accessible label.
  • Keep visible keyboard focus independent from the animation.

Avoid

  • Do not add bounce unless a real object pickup or spring settle is part of the interaction.
  • Do not hide the final state from assistive technology.
  • Do not make the user wait for animation before the underlying state updates.

Related blueprints

Browse all blueprints