forty-cdk
llms.txt

Primitives

Toast

Brief, auto-dismissing notifications stacked in a corner, opened programmatically through ForToastManager.

forty-cdk/toast WAI-ARIA APG

Fire a toast and leave it alone: hovering pauses it, a drag swipes it away, and the stack reflects data-front-stack-index so the cards behind it can shrink.

The visible toast renders with role="status" ('info' / 'success' / 'warning') or role="alert" ('error'), and screen readers announce updates through a shared off-screen live region without focus ever moving. Two ways to use the same primitive:

  • Programmatic (the common path): inject ForToastManager and call show({ title, … }) from anywhere.
  • Declarative: drop <div forToast> directly in any template, controlling mount/unmount with @if.

Mount the viewport once

In your app.html:

<for-toast-viewport [maxVisible]="5" />

The directive doesn't impose layout, so position the viewport from CSS:

[forToastViewport] {
  position: fixed;
  bottom: 1rem;
  right: 1rem;
  display: grid;
  gap: 0.5rem;
  pointer-events: none;
}
[forToast] {
  pointer-events: auto;
}

Wherever you declare it, the viewport moves itself to document.body after its first render, so a modal opened later never hides it with the rest of the page (see Toasts over a modal dialog / drawer). On the server it renders in place.

The viewport portals to document.body. CSS scoped to ancestors of <for-toast-viewport> will not apply once it moves. Style it with global CSS or with a class on the host, as above.

Scoping the viewport to a region

Bind [container] to move the viewport into a specific element instead of document.body, for toasts bounded to a panel:

<section #panel class="panel">
  <for-toast-viewport [container]="panel" />
</section>

The container must be positioned (position: relative) and the viewport position: absolute, so it is bounded to the container's box. [container] is read once, at the first render. Over an open modal a contained viewport follows its container: it stays interactive when the container is inside the modal, and is inerted with it otherwise.

Anatomy

Mount one viewport near the app root and drive it programmatically through ForToastManager:

<for-toast-viewport [maxVisible]="5" />

The viewport renders each toast in this shape (the declarative path composes the same pieces by hand inside a <div forToast>):

<div forToast variant="success" [duration]="5000">
  <div forToastTitle>Saved</div>
  <div forToastDescription>Your changes were saved.</div>
  <button forToastAction altText="Undo (Cmd+Z)">Undo</button>
  <button forToastClose>×</button>
</div>

Examples

Action & live update

An action toast carries a [forToastAction] button that runs your handler and closes with reason 'action'. The save flow shows ref.update() mutating a toast in place: 'Saving…' becomes 'Saved' with a new variant and duration, and the toast is re-announced automatically when the text changes.

Swipe right to dismiss

The viewport sets swipeDirection="right": show the toast, then drag it to the right with a mouse or a finger. It follows the pointer, a release past the 60px swipeThreshold dismisses it, and a shorter drag springs it back. Swipe-to-dismiss has the attributes and variables the CSS keys off, and the spring-back recipe.

API

ForToast

Data attributeValues
data-stateopen (always present while mounted; the consumer unmounts on close, so there is no closed state)
data-variantinfo | success | warning | error
data-pausedpresent / absent (the auto-dismiss timer is paused)
data-swipestart | move | cancel | end (absent until a swipe gesture begins)
data-swipe-directionleft | right | up | down (absent until a swipe arms)
data-front-stack-index0-based index in the visible stack (set by the viewport on the programmatic path; 0 is the front toast)

ForToastViewport

Data attributeValues
data-regionthe viewport's region name (default default)
data-toast-countnumber of toasts currently rendered

Programmatic API

import { ForToastManager } from 'forty-cdk/toast';

@Component(/* … */)
class SomeComponent {
  readonly toasts = inject(ForToastManager);

  save() {
    const ref = this.toasts.show({ title: 'Saving…', duration: 0 });
    api.save().then(
      () => ref.update({ title: 'Saved', variant: 'success', duration: 3000 }),
      () => ref.update({ title: 'Save failed', variant: 'error' }),
    );
  }

  undoableDelete(item: Item) {
    this.toasts.show({
      title: 'Item deleted',
      action: { label: 'Undo', activate: () => restore(item) },
      duration: 6000,
    });
  }
}

show() returns a ForToastRef:

  • ref.dismiss(reason?, result?): close imperatively.
  • ref.update(patch): mutate config in place (text, duration, variant). id and region are fixed at show() and ignored here. id is the toast's identity, and region decides which viewport renders it, so changing it would silently remount the toast (resetting its timer and announcement). Dismiss and re-show() to move a toast between regions.
  • ref.resetTimer(): restart the auto-dismiss countdown from the full duration. No-op once dismissed. Runs automatically when a live toast is re-shown via show({ id }) (dedupe); also callable directly to extend a toast's life on demand.
  • ref.closed: Promise<{ reason, result }> resolved on first dismiss.
  • ref.isClosed(): reactive boolean.
  • ref.config(): reactive config snapshot.

Calling show({ id }) with the id of a live toast updates it in place (dedupe) and restarts its auto-dismiss countdown from the full duration, exactly as calling ref.resetTimer() does. So a recurring identical toast (e.g. repeated "Message sent") stays visible for a fresh duration after each occurrence rather than expiring on the first one's timer.

Toasts over a modal dialog / drawer

Showing a confirmation or error toast from a flow inside a modal ForDialog / ForDrawer works out of the box. The viewport host carries data-for-modal-exempt, so an open modal automatically:

  • leaves the viewport out of its inert pass (the toast stays interactive instead of being disabled with the rest of the background), and
  • treats a click on a toast as "inside", so clicking a toast never dismisses the modal.

No wiring is needed on your side: no manual data-for-modal-peer stamping, no (pointerDownOutside) veto and no relocation. The viewport already sits directly in document.body, which is where the modal reads the marker, so one declared in your app shell stays interactive, focusable with the hotkey and audible to a screen reader. A viewport moved into a [container] follows that container instead.

Sitting behind the modal instead

The coexist-by-default above is right for confirmation / error toasts raised by the flow inside the modal. For a low-priority or system viewport that should not steal attention from a critical dialog, opt out with provideForToastDefaults({ overModal: 'inert' }):

provideForToastDefaults({ overModal: 'inert' });

The viewport then drops data-for-modal-exempt, so an open modal inerts it like any other background sibling and a click on a toast dismisses the modal. overModal resolves per injector scope, so you can keep the global default 'peer' and scope 'inert' to one viewport's subtree (or the reverse). Default is 'peer', so existing setups are unchanged.

Multiple regions

A viewport renders only the toasts whose region matches its [region] input. Omit region everywhere and everything flows through the default region. That's the single-viewport setup above. To run independent regions (e.g. system notifications top-right, action confirmations bottom-center) mount one viewport per region and tag each show():

<for-toast-viewport region="system" />
<!-- styled top-right -->
<for-toast-viewport region="confirmations" />
<!-- styled bottom-center -->
this.toasts.show({ region: 'system', title: 'New version available' });
this.toasts.show({ region: 'confirmations', title: 'Saved' });

Each region resolves to the host data-region attribute, so you can position / theme regions purely from CSS:

[forToastViewport][data-region='system'] {
  top: 1rem;
  right: 1rem;
}
[forToastViewport][data-region='confirmations'] {
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
}

If two viewports share the same region, only the first one mounted renders it; the rest stay inactive (and warn in dev) so a stray second viewport (a lazy route, a shared layout) never silently duplicates toasts. A single show() always produces exactly one toast node.

Composition + styling model

Like every other primitive, Toast is "bring your own markup + classes", even on the programmatic path where the viewport renders the markup for you. There are two levers:

Per-toast classes

Pass class (a single token or a space-separated string) or classList (a string or an array of tokens) in the show() config. They are applied to the rendered toast root (the [forToast] element) and merged with the viewport's [toastClass] and with the directive's own host attributes, so they never clobber data-state / data-variant / the swipe CSS hooks.

this.toasts.show({ title: 'Saved', variant: 'success', class: 'toast toast--success' });
this.toasts.show({ title: 'Failed', classList: ['toast', 'toast--error'] });
/* Now you can target your own class instead of the [forToast] attribute. */
.toast {
  display: grid;
  gap: 0.25rem;
  padding: 0.75rem 1rem;
  border-radius: 0.5rem;
}
.toast--success {
  border-inline-start: 4px solid green;
}

Declarative toasts (<div forToast class="toast">) take consumer classes the native way. class in show() is the programmatic equivalent. Styling by the [forToast] / [forToastTitle] / … attribute selectors still works and remains a valid choice; the class hook just unblocks design-system class names.

Exit / enter animations (programmatic)

On the programmatic path the toast root is rendered for you inside <for-toast-viewport>'s @for, so, unlike with a declarative <div forToast>, you have no node to put animate.leave on. Pass animateLeave (and, for symmetry, animateEnter) in the show() config instead, or set a viewport-wide default with [animateLeave] / [animateEnter]. The viewport binds them through Angular's native animate.leave / animate.enter on the rendered toast, so the toast stays mounted until its exit animation settles before it leaves the DOM:

this.toasts.show({ title: 'Saved', variant: 'success', animateLeave: 'toast-out' });
@keyframes toast-out {
  to {
    opacity: 0;
    transform: translateX(100%);
  }
}
[forToast].toast-out {
  animation: toast-out 200ms ease forwards;
}
@media (prefers-reduced-motion: reduce) {
  [forToast].toast-out {
    animation-duration: 1ms;
  }
}

A viewport-level default applies to every toast that omits its own; a per-toast animateLeave always wins:

<for-toast-viewport animateLeave="toast-out" animateEnter="toast-in" />

animateLeave is the load-bearing hook. A pure-CSS exit cannot defer the @for unmount, so animateLeave is the only way to play an exit animation on the programmatic path. animateEnter is optional: a plain @keyframes on [forToast] already plays on mount without it (this is why programmatic toasts have always animated in). Leaving animateLeave unset keeps the existing synchronous unmount on dismiss. prefers-reduced-motion is honored by your own CSS exactly as with the swipe / enter hooks, because the directive adds no animation of its own.

The declarative path is unchanged: write animate.leave directly on your <div forToast> (see Declarative usage).

Gliding the rest of the stack ([stackShift])

animateEnter / animateLeave cover the row that mounts or unmounts. They do not cover its siblings: when a toast is added or dismissed, the rest of the stack is re-laid-out by reflow and snaps to its new spot in a single frame. Nothing about those rows changed, so there is no property for your CSS to transition.

[stackShift] opts into a glide for exactly those rows. The viewport measures the new layout, offsets each moved row back to where it was and animates it to zero (FLIP):

<for-toast-viewport [stackShift]="{ duration: 200, easing: 'cubic-bezier(0.05, 0.7, 0.1, 1)' }" />

A bare number is shorthand for { duration } with linear easing:

<for-toast-viewport [stackShift]="200" />

Four things are worth knowing:

  • The library drives translate, transform stays yours. The glide is played on the individual translate property, which the browser applies before transform, so the two compose. Nothing above changes: the exit keyframes on transform and the swipe recipe's transform: translate3d(var(--for-toast-swipe-movement-x), …) keep working with [stackShift] set, including while a row is mid-glide. The one thing to avoid is writing your own translate on [forToast]. An animation outranks every author declaration of the same property, inline ones included, so the glide would suppress yours while it played.
  • Nothing moves by default. Leaving [stackShift] unset keeps today's synchronous reflow, and [stackShift]="0" opts a single viewport out of a scope-level default.
  • prefers-reduced-motion: reduce suppresses it: the library skips the glide entirely, no consumer CSS needed. This is the one animation hook the directive gates for you, because the directive owns the motion rather than handing you a class.
  • It is the programmatic path only. On the declarative path the rows and their container are yours, so a directive of your own on that container is the right level.

Set it for a whole scope with provideForToastDefaults({ stackShift: 200 }); a viewport's own [stackShift] wins.

Custom rendering with a template

If the default title / description / action / close shape isn't enough, pass a template:

<ng-template #toastTpl let-toast let-data="data">
  <span>{{ data.user.name }} liked your post</span>
  <button type="button" (click)="goToPost(data.post); toast.dismiss()">View</button>
</ng-template>
this.toasts.show({ template: this.toastTpl, data: { user, post } });

The template context is { $implicit: ForToastInstance, data: T }. Use toast.dismiss() to close from inside the template.

The helper directives work inside a custom template. The viewport renders the template with the [forToast] injection context in scope, so [forToastTitle], [forToastDescription], [forToastAction], and [forToastClose] keep their automatic aria-labelledby / aria-describedby / close-reason wiring, exactly as in the default shape. Just import the directives into the component that declares the <ng-template>:

@Component({
  imports: [ForToastViewport, ForToastTitle, ForToastDescription, ForToastAction, ForToastClose],
  // …
})
<ng-template #toastTpl let-toast let-data="data">
  <div forToastTitle class="toast__title">{{ data.title }}</div>
  <div forToastDescription class="toast__desc">{{ data.body }}</div>
  <button forToastAction altText="Undo (Cmd+Z)" (click)="restore(data.item)">Undo</button>
  <button forToastClose class="toast__close" aria-label="Dismiss">×</button>
</ng-template>

[forToastAction] / [forToastClose] emit (dismiss) (reason 'action' / 'manual') through the same context as the default shape, so there is no need to call toast.dismiss() manually for those. (toast.dismiss() from $implicit is still available for arbitrary buttons that aren't action / close.) This combines with per-toast class: add a class for the root and your own classes on the helper elements.

Viewport-wide defaults

An app with one toast design sets it once on the viewport instead of on every show(). Four viewport inputs default a per-toast config field for every toast the viewport renders:

Viewport inputPer-toast fieldWhen both are set
[template]templatethe per-toast template wins
[toastClass]class / classListthe classes merge
[animateEnter]animateEnterthe per-toast class wins
[animateLeave]animateLeavethe per-toast class wins
<for-toast-viewport [template]="toastTpl" toastClass="toast" animateLeave="toast-out" />

<ng-template #toastTpl let-toast>
  <div forToastTitle>{{ toast.config.title }}</div>
  <button forToastClose aria-label="Dismiss">×</button>
</ng-template>

The viewport resolves them while it renders, so a toast shown before its first render (an error raised while the app bootstraps, say) still gets them. The class merges rather than replaces because a base class on the viewport plus a modifier per toast is the common split: show({ class: 'toast--error' }) renders a root carrying both toast and toast--error.

Declarative usage

For a toast driven by component state (e.g. an offline banner):

@if (offline()) {
<div
  forToast
  variant="warning"
  [duration]="0"
  (dismiss)="offline.set(false)"
  animate.leave="fade-out"
>
  <div forToastTitle>Network unavailable</div>
  <div forToastDescription>Reconnecting…</div>
</div>
}

The directive doesn't manage its own visibility; @if does. The directive emits (dismiss) when the timer / Escape / action / close button want it gone; the consumer reacts by flipping the gate.

Swipe-to-dismiss

Optional, opt-in. Set [swipeDirection] on a declarative toast (or via swipeDirection in the programmatic config / on the viewport) to let the user drag the toast off-screen with a touch or mouse pointer. The gesture uses pointer events, so it works on every input device.

<div forToast swipeDirection="right" [swipeThreshold]="60" (dismiss)="dismiss()">…</div>

swipeDirection accepts a single direction ('left' | 'right' | 'up' | 'down') or an array of directions. The dominant axis of the user's drag picks which one wins; gestures perpendicular to every allowed direction are dropped. The dismiss commits when pointer-up happens past swipeThreshold pixels of pointer travel along the active direction (default 50).

While the gesture is live the host carries:

Attribute / variableValuesPurpose
data-swipe"start" | "move" | "cancel" | "end"Lifecycle marker: "start" marks the move that armed the gesture until the next move replaces it with "move". "end" means "about to fire (dismiss) with reason swipe". "cancel" is parked (with the released movement vars) so your CSS can spring the toast back, then cleared on the next pointerdown.
data-swipe-direction"left" | "right" | "up" | "down"Direction the gesture armed in.
--for-toast-swipe-movement-x (px)continuousHorizontal pointer travel, clamped to the half-line of the active direction.
--for-toast-swipe-movement-y (px)continuousVertical pointer travel, clamped to the half-line of the active direction.

The directive does NOT animate anything. The consumer's CSS transitions / animate.leave drive the visual feedback:

[forToast] {
  transition: transform 200ms ease-out;
  transform: translate3d(
    var(--for-toast-swipe-movement-x, 0px),
    var(--for-toast-swipe-movement-y, 0px),
    0
  );
}
[forToast][data-swipe='move'] {
  transition: none;
}
[forToast][data-swipe='cancel'] {
  transform: translate3d(0, 0, 0);
}

Spring the toast back by resetting transform on [data-swipe='cancel'], not by zeroing the --for-toast-swipe-movement-* variables. The directive publishes those variables as inline styles on the host and holds them at the released delta through the cancel, so a stylesheet rule that tries to set them to 0 loses to the inline value and has no effect (the toast would stay stuck at the release position). Overriding transform sidesteps the inline variables entirely. After a cancel the host keeps data-swipe="cancel" and the parked movement vars so the transition above can run on its own timeline; the directive then clears that parked state (data-swipe removed, movement vars zeroed) on the next pointerdown, so a stale cancel never bleeds into the next gesture or lingers after a CSS-less consumer. A re-armed swipe overwrites it anyway.

Outputs:

  • (swipeStart): armed; emitted once with { direction, delta, originalEvent }.
  • (swipeMove): every pointer move while active.
  • (swipeEnd): released past threshold (immediately followed by (dismiss) with reason 'swipe').
  • (swipeCancel): released before threshold, or pointercancel.

dismissible=false disables swipe entirely: a sticky / forced-action toast cannot be user-dismissed.

Auto-dismiss + pause-on-hover

  • Timer starts on mount and fires (dismiss) with reason 'auto' after duration ms.
  • Hovering or focusing inside the toast pauses the timer; leaving / blurring resumes with the remaining time.
  • The timer also pauses while document.visibilityState !== 'visible' (tab backgrounded, window hidden) and resumes when the page becomes visible again, so toasts don't silently expire while the user is not looking. The visibilitychange listener is shared across all live toasts (refcounted), so there is one document-level handler regardless of stack depth.
  • A hover/focus/visibility pause captures the remaining time and resumes with it. A ref.update() that does not change duration preserves that captured countdown, so resume continues from where it paused. Changing duration (via ref.update({ duration }) or a [duration] change) restarts the countdown at the full new duration; if the toast is paused when the change lands, the restart is applied on resume.
  • duration: 0 keeps the toast sticky, so only manual / action / programmatic close ends it.

maxVisible parks overflow, it does not expire it. A toast pushed out of the visible window by [maxVisible] is unmounted, so its auto-dismiss timer is not running while it waits. When a newer toast is dismissed it re-enters the window and its duration countdown restarts from full (a fresh [forToast] mounts). If you need overflow toasts to clear on a deadline, dismiss them explicitly (ForToastRef.dismiss() / dismissAll() / the action / close button) rather than relying on the timer.

Variants

VariantRoleAnnouncedUse for
info (default)statuspoliteNeutral notifications.
successstatuspoliteConfirmations of completed actions.
warningstatuspoliteNon-blocking warnings.
erroralertassertiveFailures that interrupt the user's task.

The Announced column is the politeness a screen reader hears. It is delivered through a shared off-screen live region (see Live updates and announcements), so the host's own aria-live is off for every variant except a bare error. data-variant is reflected on the host so consumers can paint per-variant icons / colors purely from CSS.

Scoped defaults

provideForToastDefaults configures defaults for an injector subtree: at the application root, or in any component's providers array, where a partial override inherits the keys it does not name.

import { provideForToastDefaults } from 'forty-cdk/defaults';

bootstrapApplication(App, {
  providers: [provideForToastDefaults({ duration: 4000, hotkey: 'F6', maxVisible: 5 })],
});

Per-viewport overrides take precedence: <for-toast-viewport [maxVisible]="3" hotkey="F8" />.

viewportAriaLabel (default 'Notifications') is the localizable accessible name of every viewport in the scope; [ariaLabel] overrides it per viewport. closeAriaLabel (default 'Close') names every [forToastClose] in the scope the same way, and its [ariaLabel] overrides it per button. For a language the app sets or switches after bootstrap, pass the text keys as functions and the overrides as a factory, as Localizing default text shows.

overModal ('peer' | 'inert', default 'peer') is also a defaults key. See Sitting behind the modal instead.

stackShift (default null) sets the sibling glide for every viewport in the scope. See Gliding the rest of the stack.

Keyboard

  • Toast announcements never steal focus. The user keeps typing.
  • The configured hotkey (default F6) anywhere in the document focuses the first visible toast.
  • Dismissing the toast that holds focus (Escape, the close button, the action button, or ref.dismiss()) moves focus to the next visible toast, else the previous one, else back to the element that had focus when the hotkey was pressed, else the viewport. An action handler that moves focus somewhere else keeps it there.
  • Inside a toast: Tab cycles between action / close buttons; Escape dismisses (when dismissible); Shift+Tab returns out.

Accessibility

Implements the WAI-ARIA Alert pattern.

  • aria-atomic="true" on the toast means that when the host's own aria-live region announces (as it does for a bare error toast, the one variant still announced by its host), the screen reader reads the whole toast rather than only the changed node. On every other variant the host aria-live is off and announcements route through the shared LiveAnnouncer, so aria-atomic is inert there. Re-announcement on a ref.update() text change is driven explicitly (see Live updates and announcements below).

  • aria-labelledby and aria-describedby wire automatically from [forToastTitle] / [forToastDescription]. Multiple titles / descriptions concatenate ids.

  • role="alert" (variant error) interrupts the screen reader queue; reserve it for genuinely interrupting messages.

  • The viewport's role="region" with aria-label makes it discoverable in landmark navigation; the F6 hotkey is the standard "jump to notifications" shortcut. Name it with [ariaLabel] per viewport, or with provideForToastDefaults({ viewportAriaLabel: '…' }) to translate every viewport in the scope (default Notifications). [ariaLabel]="null" drops the attribute; a static aria-label on the host replaces both channels, so it wins over a null too.

  • Pause on hover / focus is mandated by WCAG 2.1 SC 2.2.1 for time-limited content.

  • Action buttons should set [altText] whenever the visible label (e.g. "Undo") wouldn't tell a user how to recover the action after the toast disappears. The altText is folded into the synthesized announcement (title. description. altText), which meets WCAG SC 2.2.1 for non-recoverable, time-limited actions. It also switches a bare error toast onto the LiveAnnouncer path, since the recovery hint is not in the visible DOM the host region would read.

    <button forToastAction altText="Undo (Cmd+Z)" (click)="restore()">Undo</button>

Live updates and announcements

A toast announces on one of two paths, picked automatically:

  • LiveAnnouncer (default). Every variant except a bare error silences its host aria-live (off) and pushes a composed message (title. description. altText) through the shared off-screen LiveAnnouncer. Its two politeness regions exist in the accessibility tree before the toast mounts, so the announcement is reliable. A live region has to exist before its content changes to be read, and a toast is always inserted with its content already present, which makes announcing from the host itself unreliable (info / success / warning especially).
  • Host role="alert" (bare error). An error toast with no action altText keeps its own host as the live region (role="alert", aria-live="assertive"): alert is the one live role screen readers read reliably on insertion. That holds only for a toast inserted as a bare error. As soon as such a toast carries an altText, it joins the LiveAnnouncer (assertive) path so the recovery hint, which is absent from the visible DOM, is still voiced, and a toast that becomes an error after it mounts (a ref.update({ variant: 'error' })) is announced on that path as well, with role="alert" and its host aria-live off. Neither moves back to the host.

The LiveAnnouncer path is reactive. A late-bound altText (set after first render) and any ref.update() that changes the title, description, or altText trigger a re-announce. The composed message is tracked together with its politeness: an unchanged message never re-fires, and a variant change into or out of error re-announces it at the new politeness. This is why the contract is "drive announcements explicitly", not "trust aria-atomic": aria-atomic does nothing on the LiveAnnouncer path, so the directive owns the re-announce.

const ref = this.toasts.show({ title: 'Saving…', duration: 0 });
// Re-announced automatically when the text changes:
await api.save();
ref.update({ title: 'Saved', variant: 'success', duration: 3000 });

Styling

forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).

Toast pieces ([forToast], [forToastTitle], [forToastDescription], [forToastAction], [forToastClose]) are rendered inside the library's <for-toast-viewport> component on the programmatic path, so they cannot take a consumer class directly. Style them with global attribute selectors (e.g. [forToast][data-variant='error']). The exceptions are the viewport's [toastClass] and the per-toast class / classList in the show() config, which the viewport applies to the [forToast] root for you (see Viewport-wide defaults and Per-toast classes). Only <for-toast-viewport> itself lives in the consumer's own template, so it is the one element that can take an ordinary class; it moves to document.body, so style that class globally. Declarative toasts (<div forToast class="…">) take consumer classes the native way.

CSS custom properties

Written on the [forToast] host while a swipe gesture is live, so the consumer can drive a transform-based animation entirely from CSS.

PropertyMeaning
--for-toast-swipe-movement-xHorizontal pointer travel in px, clamped to the active swipe direction.
--for-toast-swipe-movement-yVertical pointer travel in px, clamped to the active swipe direction.
[forToast] {
  transition: transform 200ms ease-out;
  transform: translate3d(
    var(--for-toast-swipe-movement-x, 0px),
    var(--for-toast-swipe-movement-y, 0px),
    0
  );
}
[forToast][data-swipe='move'] {
  transition: none;
}
[forToast][data-swipe='cancel'] {
  transform: translate3d(0, 0, 0);
}
[forToast][data-variant='error'] {
  border-inline-start: 4px solid red;
}

Wrapping in a design system

Subclass the root and re-provide FOR_TOAST_CONTEXT with useExisting pointing at the subclass, since Angular does not inherit a directive's providers; Wrapping non-form roots walks the pattern.