Composition (children / slot / snippet)
Toast notifications
A reusable Toast wrapper that takes arbitrary content. The Toaster fires one for every add-to-cart and wishlist toggle. The interesting comparison is how each framework lets a parent pass markup into a child slot.
Each panel header shows the source size of that component file (raw, gzipped, line count). Source-only — framework runtime (React ~45 KB, Vue ~35 KB, Svelte ~5 KB gzipped) is shared across every island and isn't included in these numbers.
How this works
children is just a typed prop — ReactNode covers strings, elements, fragments, anything. The wrapper renders {children} where the slot belongs. The Toaster has no enter/leave animation — for that, you would install framer-motion (<AnimatePresence>) or react-transition-group.
import type { ReactNode } from 'react';
import type { ToastVariant } from '../stores/toasts';
type Props = {
variant?: ToastVariant;
onDismiss?: () => void;
children: ReactNode;
};
const variantClasses: Record<ToastVariant, string> = {
info: 'border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100',
success: 'border-emerald-200 bg-emerald-50 text-emerald-900',
wishlist: 'border-rose-200 bg-rose-50 text-rose-900',
};
export default function ToastReact({ variant = 'info', onDismiss, children }: Props) {
return (
<div
role="status"
className={`flex items-start gap-3 rounded-md border px-3 py-2 text-sm shadow-md ${variantClasses[variant]}`}
>
<div className="flex-1">{children}</div>
{onDismiss && (
<button
type="button"
onClick={onDismiss}
aria-label="Dismiss"
className="text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
>
✕
</button>
)}
</div>
);
} How this works
A default <slot /> in the template renders whatever the parent put between <Toast>...</Toast>. The Toaster wraps its v-for in <TransitionGroup name="toast" tag="div"> and a <style scoped> defines toast-enter/leave classes for slide-in/out — built-in, no library.
<script setup lang="ts">
import type { ToastVariant } from '../stores/toasts';
const props = withDefaults(
defineProps<{
variant?: ToastVariant;
onDismiss?: () => void;
}>(),
{ variant: 'info', onDismiss: undefined },
);
const variantClasses: Record<ToastVariant, string> = {
info: 'border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100',
success: 'border-emerald-200 bg-emerald-50 text-emerald-900',
wishlist: 'border-rose-200 bg-rose-50 text-rose-900',
};
</script>
<template>
<div
role="status"
:class="[
'flex items-start gap-3 rounded-md border px-3 py-2 text-sm shadow-md',
variantClasses[props.variant],
]"
>
<div class="flex-1">
<slot />
</div>
<button
v-if="props.onDismiss"
type="button"
@click="props.onDismiss?.()"
aria-label="Dismiss"
class="text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
>✕</button>
</div>
</template> How this works
Svelte 5 replaces the old <slot /> with snippets. Children come in as a Snippet prop and are rendered with {@render children()}. The Toaster adds transition:fly={{ x: 20, duration: 200 }} as a one-liner on each toast wrapper — Svelte ships transition primitives in svelte/transition, no library, no CSS classes.
<script lang="ts">
import type { Snippet } from 'svelte';
import type { ToastVariant } from '../stores/toasts';
let {
variant = 'info',
onDismiss,
children,
}: {
variant?: ToastVariant;
onDismiss?: () => void;
children: Snippet;
} = $props();
const variantClasses: Record<ToastVariant, string> = {
info: 'border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100',
success: 'border-emerald-200 bg-emerald-50 text-emerald-900',
wishlist: 'border-rose-200 bg-rose-50 text-rose-900',
};
</script>
<div
role="status"
class={`flex items-start gap-3 rounded-md border px-3 py-2 text-sm shadow-md ${variantClasses[variant]}`}
>
<div class="flex-1">
{@render children()}
</div>
{#if onDismiss}
<button
type="button"
onclick={() => onDismiss?.()}
aria-label="Dismiss"
class="text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"
>✕</button>
{/if}
</div>