# How Astryx Toast System Handles Collision Detection and Positioning for Multiple Toasts

> Discover how Astryx's toast system prevents collisions and positions multiple toasts efficiently using ToastViewport, uniqueID deduplication, and flexbox stacking.

- Repository: [Meta/astryx](https://github.com/facebook/astryx)
- Tags: internals
- Published: 2026-08-04

---

**Astryx's toast system uses a single `ToastViewport` component with `uniqueID`-based deduplication and flexbox-based stacking to manage collision detection and positioning for multiple concurrent notifications.**

Toast notifications often overlap or duplicate when users trigger rapid actions. The `facebook/astryx` repository solves this through a centralized viewport pattern. The `ToastViewport` component in [`packages/core/src/Toast/ToastViewport.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Toast/ToastViewport.tsx) orchestrates collision detection via configurable behaviors and positions toasts using CSS flexbox with directional stacking logic.

## Collision Detection via Unique IDs and Behaviors

Every toast can carry an optional **`uniqueID`** in its `ToastOptions`. When `addToast` is called, the system checks this ID against existing toasts and applies a **`collisionBehavior`** to determine the outcome.

### Available Collision Behaviors

The `ToastCollisionBehavior` type (defined in [`packages/core/src/Toast/types.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/Toast/types.ts), lines 15–17) supports two modes:

- **`overwrite`** — The new toast replaces the existing one with the same `uniqueID`, preserving position in the stack
- **`ignore`** — The new toast is suppressed entirely, preventing any render or screen-reader announcement

### Implementation in ToastViewport.tsx

Lines 205–231 of [`ToastViewport.tsx`](https://github.com/facebook/astryx/blob/main/ToastViewport.tsx) contain the core detection logic:

```typescript
// Simplified excerpt from ToastViewport.tsx lines 205-231
function addToast(options: ToastOptions) {
  const existingIndex = findToastIndexByUniqueID(options.uniqueID);
  
  if (existingIndex !== -1) {
    if (options.collisionBehavior === 'ignore') {
      return; // Early exit — toast suppressed
    }
    if (options.collisionBehavior === 'overwrite') {
      // Replace in-place rather than appending
      updateToastAtIndex(existingIndex, options);
      return;
    }
  }
  
  // No collision — append new toast
  appendToast(options);
}

```

This deduplication happens before any render cycle, keeping the DOM and accessibility tree clean.

## Positioning and Stacking Strategy

### Viewport Position Prop

`ToastViewport` accepts a **`position`** prop with four `ToastPosition` values:

- `topEnd`
- `topStart`
- `bottomEnd`
- `bottomStart`

### CSS Flexbox Mapping

Lines 43–56 of [`ToastViewport.tsx`](https://github.com/facebook/astryx/blob/main/ToastViewport.tsx) define static style objects for each position:

```typescript
// From ToastViewport.tsx lines 43-56
const bottomEnd = {
  alignItems: 'flex-end',
  bottom: 16,
  right: 16,
};

const topEnd = {
  alignItems: 'flex-end',
  top: 16,
  right: 16,
  flexDirection: 'column-reverse', // Newest at top
};

// bottomStart and topStart follow same pattern with left alignment

```

The component selects styles via `posStyle` and applies them through `stylex.props`.

### Stack Order Control

| Position | Flex Direction | Newest Toast Appears At |
|----------|---------------|------------------------|
| `topEnd`, `topStart` | `column-reverse` | Top of stack |
| `bottomEnd`, `bottomStart` | `column` (default) | Bottom of stack |

This ensures visual consistency: top positions grow downward, bottom positions grow upward.

### Visible Toast Limiting

The viewport respects **`maxVisible`** to prevent screen overflow:

```typescript
// From ToastViewport.tsx — visible slice logic
const visibleToasts = toasts.slice(-maxVisible);

```

Only the last `maxVisible` toasts render; older entries remain in state but hidden.

## Complete Usage Examples

### Overwriting Duplicate Status Messages

```tsx
import { useToast } from '@astryxdesign/core';

function SaveButton() {
  const showToast = useToast();

  const handleSave = async () => {
    // Show pending state
    showToast({
      body: 'Saving changes...',
      uniqueID: 'save-status',
      type: 'loading',
    });

    await saveToServer();

    // Replace with success — same uniqueID triggers overwrite
    showToast({
      body: 'Saved successfully',
      uniqueID: 'save-status',
      type: 'success',
    });
  };

  return <button onClick={handleSave}>Save</button>;
}

```

### Ignoring Duplicate Warnings

```tsx
function WarnUnsavedChanges() {
  const showToast = useToast();

  // First call shows toast; subsequent calls suppressed
  const warn = () => {
    showToast({
      body: 'You have unsaved changes',
      uniqueID: 'unsaved-changes',
      collisionBehavior: 'ignore',
      type: 'warning',
    });
  };

  return <button onClick={warn}>Check Status</button>;
}

```

### Configuring Viewport Position and Limit

```tsx
import { ToastViewport } from '@astryxdesign/core';

function App() {
  return (
    <ToastViewport 
      position="topEnd" 
      maxVisible={5}
    >
      <Router />
    </ToastViewport>
  );
}

```

## Key Implementation Files

| File | Responsibility |
|------|---------------|
| [`packages/core/src/Toast/ToastViewport.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Toast/ToastViewport.tsx) | Collision detection (lines 205–231), position styles (lines 43–56), stacking logic |
| [`packages/core/src/Toast/types.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/Toast/types.ts) | `ToastCollisionBehavior`, `ToastPosition`, and option interfaces (lines 15–17) |
| [`packages/core/src/Toast/ToastContext.ts`](https://github.com/facebook/astryx/blob/main/packages/core/src/Toast/ToastContext.ts) | Context provider with `addToast`, `removeToast`, `findByUniqueID` |
| [`packages/core/src/Toast/Toast.tsx`](https://github.com/facebook/astryx/blob/main/packages/core/src/Toast/Toast.tsx) | Individual toast rendering, auto-hide timers, dismiss UI |

## Summary

- **Collision detection** relies on `uniqueID` matching with configurable `collisionBehavior` of `overwrite` or `ignore`
- **Positioning** uses four viewport corners with flexbox alignment and inset placement
- **Stack order** inverts via `column-reverse` for top positions so newest toasts appear highest
- **Overflow protection** slices toasts to `maxVisible`, keeping only the most recent entries rendered
- All logic centralizes in [`ToastViewport.tsx`](https://github.com/facebook/astryx/blob/main/ToastViewport.tsx) according to the Astryx source code architecture

## Frequently Asked Questions

### How does Astryx prevent duplicate toast spam?

Astryx prevents spam through the `ignore` collision behavior. When a toast with `collisionBehavior: 'ignore'` shares a `uniqueID` with an existing toast, the `addToast` function returns early at line 205–231 of [`ToastViewport.tsx`](https://github.com/facebook/astryx/blob/main/ToastViewport.tsx) without adding or announcing anything.

### Can I change toast position dynamically?

The `position` prop accepts `topEnd`, `topStart`, `bottomEnd`, or `bottomStart` as static values. Dynamic position changes would require remounting `ToastViewport` or maintaining multiple viewports, as the position styles are computed at render time from the static objects defined at lines 43–56.

### What happens when maxVisible is exceeded?

Older toasts are sliced from the visible array via `toasts.slice(-maxVisible)` while remaining in internal state. They reappear if newer toasts are dismissed, maintaining FIFO order within the visibility window.

### Why does column-reverse matter for top positions?

Without `column-reverse`, flexbox would append new toasts below existing ones for top positions, causing the stack to grow away from the viewport edge. The reversal ensures visual anchoring: top positions grow downward from the corner, bottom positions grow upward.