Summary
Add a built-in toast notification system to stx with two parts:
<StxToast />— a builtin component (like<Icon />,<StxLink />) that renders the toast containertoast()— a runtime global function (likestate(),onMount()) for triggering toasts from any<script client>block
Usage
Layout (place once)
<!-- resources/layouts/app.stx -->
@include('styles')
@include('theme')
@include('nav')
<main>
@slot('content')
</main>
<StxToast /><StxToast /> renders an empty container (top-right fixed position) that toasts get appended to. It should be placed once in a layout — not per-page.
Triggering toasts
<script client>
// Available as a runtime global (no import needed in <script client>)
toast.success('Settings saved!')
toast.error('Failed to save')
toast.info('Syncing your data...')
toast.warning('You are offline')
// With options
toast.success('Saved!', { duration: 5000 })
toast.error('Network error', { duration: 0 }) // persistent until dismissed
</script>Importable composable (for store files)
// resources/stores/auth.ts
import { toast } from '@stacksjs/stx/composables'
// or if auto-imported in <script client>, just use toast directlyAPI
toast global
interface ToastOptions {
/** Duration in ms before auto-dismiss. Default: 3000. Set 0 for persistent. */
duration?: number
/** Position. Default: 'top-right' */
position?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center'
}
toast.success(message: string, options?: ToastOptions): void
toast.error(message: string, options?: ToastOptions): void
toast.info(message: string, options?: ToastOptions): void
toast.warning(message: string, options?: ToastOptions): void
toast.dismiss(id?: string): void // dismiss specific or all<StxToast /> component
Props (optional):
position— default position for all toasts (default:'top-right')max— max visible toasts (default: 5, older ones get dismissed)
<StxToast position="top-right" max="5" />Rendered HTML (injected at build time):
<div id="stx-toast-container" class="fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none" style="max-width: 400px;">
<!-- toasts get appended here dynamically -->
</div>Each toast element:
<div class="stx-toast stx-toast-success pointer-events-auto flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg bg-white dark:bg-slate-800 border border-gray-200 dark:border-slate-700 animate-slide-in" role="alert">
<span class="stx-toast-icon shrink-0"><!-- success/error/info/warning icon --></span>
<p class="stx-toast-message flex-1 text-sm text-gray-900 dark:text-white">Settings saved!</p>
<button class="stx-toast-close shrink-0 text-gray-400 hover:text-gray-600" aria-label="Close">×</button>
</div>Implementation notes
Runtime (signals.ts)
Add toast to the global scope alongside state, onMount, etc:
// In the IIFE that sets up globals
const toastQueue = []
let toastContainer = null
window.toast = {
success(msg, opts) { addToast('success', msg, opts) },
error(msg, opts) { addToast('error', msg, opts) },
info(msg, opts) { addToast('info', msg, opts) },
warning(msg, opts) { addToast('warning', msg, opts) },
dismiss(id) { removeToast(id) },
}
function addToast(type, message, options = {}) {
const id = 'toast-' + Date.now() + '-' + Math.random().toString(36).slice(2)
const duration = options.duration ?? 3000
// Find container (created by <StxToast /> builtin)
if (!toastContainer) toastContainer = document.getElementById('stx-toast-container')
if (!toastContainer) return // no <StxToast /> in layout
// Create toast element
const el = document.createElement('div')
el.id = id
el.className = 'stx-toast stx-toast-' + type + ' ...'
el.innerHTML = '...'
toastContainer.appendChild(el)
// Auto-dismiss
if (duration > 0) setTimeout(() => removeToast(id), duration)
// Dismiss on click
el.querySelector('.stx-toast-close')?.addEventListener('click', () => removeToast(id))
}
function removeToast(id) {
const el = document.getElementById(id)
if (el) {
el.classList.add('animate-slide-out')
setTimeout(() => el.remove(), 300)
}
}Builtin component (builtins/toast.ts)
Like builtins/icon.ts, register a StxToast builtin that renders the container div at template processing time:
export const toastBuiltin: BuiltinComponentDef = {
name: 'StxToast',
selfClosing: true,
render(props) {
const position = props.position || 'top-right'
const positionClasses = {
'top-right': 'top-4 right-4',
'top-left': 'top-4 left-4',
'bottom-right': 'bottom-4 right-4',
'bottom-left': 'bottom-4 left-4',
'top-center': 'top-4 left-1/2 -translate-x-1/2',
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2',
}
return `<div id="stx-toast-container" class="fixed ${positionClasses[position]} z-[9999] flex flex-col gap-2 pointer-events-none" style="max-width:400px"></div>`
},
}Composable (composables/use-toast.ts)
For store files that can't access runtime globals:
export function useToast() {
return {
success: (msg: string, opts?: ToastOptions) => (window as any).toast?.success(msg, opts),
error: (msg: string, opts?: ToastOptions) => (window as any).toast?.error(msg, opts),
info: (msg: string, opts?: ToastOptions) => (window as any).toast?.info(msg, opts),
warning: (msg: string, opts?: ToastOptions) => (window as any).toast?.warning(msg, opts),
dismiss: (id?: string) => (window as any).toast?.dismiss(id),
}
}CSS animations
Include these in the signals runtime CSS injection (or Crosswind defaults):
@keyframes stx-slide-in {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes stx-slide-out {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.stx-toast { animation: stx-slide-in 0.3s ease-out; }
.stx-toast-dismiss { animation: stx-slide-out 0.3s ease-in forwards; }Toast type styling
Each type gets an accent color on the left border or icon:
- success — green left border, checkmark icon
- error — red left border, x-circle icon
- info — blue left border, info icon
- warning — yellow left border, alert-triangle icon
These should use Crosswind utility classes and work with dark mode.
Files to create/modify
packages/stx/src/builtins/toast.ts— builtin component registrationpackages/stx/src/builtins/index.ts— register the toast builtinpackages/stx/src/signals.ts— addtoastglobal +addToast/removeToastfunctionspackages/stx/src/composables/use-toast.ts— composable wrapperpackages/stx/src/composables/index.ts— exportuseToast
Why built-in vs userland
Toast is one of those primitives every app needs. Making it a builtin means:
- Zero dependencies (no external toast library)
- Works with stx's signals reactivity out of the box
- Consistent dark mode support via Crosswind
- Available as a runtime global — no import ceremony in
<script client>blocks - Tiny footprint — just DOM manipulation, no virtual DOM