Skip to content

← all comparisons

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.

React 529 B gzip · 36 lines
ToastReact.tsx
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>
  );
}
Vue 531 B gzip · 39 lines
ToastVue.vue
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>
Svelte 519 B gzip · 38 lines
ToastSvelte.svelte
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>