How Astryx Toast System Handles Collision Detection and Positioning for Multiple Toasts
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 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, lines 15–17) supports two modes:
overwrite— The new toast replaces the existing one with the sameuniqueID, preserving position in the stackignore— The new toast is suppressed entirely, preventing any render or screen-reader announcement
Implementation in ToastViewport.tsx
Lines 205–231 of ToastViewport.tsx contain the core detection logic:
// 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:
topEndtopStartbottomEndbottomStart
CSS Flexbox Mapping
Lines 43–56 of ToastViewport.tsx define static style objects for each position:
// 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:
// 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
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
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
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 |
Collision detection (lines 205–231), position styles (lines 43–56), stacking logic |
packages/core/src/Toast/types.ts |
ToastCollisionBehavior, ToastPosition, and option interfaces (lines 15–17) |
packages/core/src/Toast/ToastContext.ts |
Context provider with addToast, removeToast, findByUniqueID |
packages/core/src/Toast/Toast.tsx |
Individual toast rendering, auto-hide timers, dismiss UI |
Summary
- Collision detection relies on
uniqueIDmatching with configurablecollisionBehaviorofoverwriteorignore - Positioning uses four viewport corners with flexbox alignment and inset placement
- Stack order inverts via
column-reversefor 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.tsxaccording 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →