Skip to content

Notification toast component

Problem. Reusable toast Web Component with auto-dismiss and reduced-motion respect.

Solution. Define a Component with css styles and Motion helpers.

ts
import { defineComponent, css, html } from '@bquery/bquery/component';
import { animate } from '@bquery/bquery/motion';
import { prefersReducedMotion } from '@bquery/bquery/a11y';

defineComponent('ds-toast', {
  props: { duration: { type: Number, default: 3000 }, message: { type: String, default: '' } },
  state: { open: true },
  styles: css`
    :host { position: fixed; bottom: 1rem; right: 1rem; background: #222; color: white;
            padding: .75rem 1rem; border-radius: 6px; display: none; }
    :host([open]) { display: block; }
  `,
  connected() {
    this.toggleAttribute('open', true);

    const reducedMotion = prefersReducedMotion();
    if (!reducedMotion.value) {
      animate(this, { transform: ['translateY(20px)', 'translateY(0)'] }, { duration: 180 });
    }
    reducedMotion.destroy();

    const host = this as HTMLElement & { dismissTimer?: ReturnType<typeof setTimeout> };
    host.dismissTimer = setTimeout(
      () => this.setState('open', false),
      this.getProp<number>('duration')
    );
  },
  updated() {
    this.toggleAttribute('open', this.getState('open'));
  },
  disconnected() {
    const host = this as HTMLElement & { dismissTimer?: ReturnType<typeof setTimeout> };
    if (host.dismissTimer) clearTimeout(host.dismissTimer);
  },
  render({ props }) {
    return html`<span role="status">${props.message}</span>`;
  },
});

Why it works. role="status" makes the message accessible without forcing focus; reduced-motion users skip the slide-in.

Released under the MIT License.