Your own cross-framework component registry — a custom-built toast you install into React & Vue with one command.
Copy, paste, own it. Same design, two frameworks.
- Cross-framework — one registry, two authored sources (
.tsx+.vue). - Custom-built — written from scratch on framework primitives (no
sonner/vue-sonner). - Rich colors — semantic success / error / warning / info, carried by a squared icon tile.
- One shape — a single squared card: icon tile beside the header, description on its own full-width line.
- Directional motion — toasts exit toward their anchor; swipe to dismiss (sideways for corners, up/down for centered).
- Examples — default, types, description, action, promise, progress, custom content, positions.
- Theme-aware — light & dark via CSS variables.
- Accessible —
aria-liveannouncements, keyboard-focusable dismiss, honorsprefers-reduced-motion.
The registry is served as JSON. Add it with the shadcn CLI:
# React
npx shadcn@latest add https://sapa-registry.vercel.app/r/react/toaster.json
# Vue
npx shadcn-vue@latest add https://sapa-registry.vercel.app/r/vue/toaster.jsonFiles land in components/ui/sapa-toast/. Optional demo examples:
.../r/react/toast-types.json, toast-promise.json, toast-positions.json, …
Mount the toaster once, then call the API anywhere:
// React — e.g. app/layout.tsx
import { Toaster, toast } from "@/components/ui/sapa-toast/toaster";
// <Toaster position="bottom-right" />
toast.success("Saved", { description: "All changes stored." });<!-- Vue — e.g. App.vue -->
<script setup>
import Toaster from "@/components/ui/sapa-toast/Toaster.vue";
import { toast } from "@/components/ui/sapa-toast/useToast";
</script>
<template>
<Toaster position="bottom-right" />
</template>
<!-- toast.success("Saved", { description: "All changes stored." }) -->toast(msg, opts) · toast.success / error / warning / info(msg, opts) ·
toast.loading · toast.progress(msg, { value }) ·
toast.promise(p, { loading, success, error }) ·
toast.custom(node/component) · toast.dismiss(id?)
Options: title, description, duration (Infinity to persist), position,
action, cancel, icon, progress (0–100).
Every toast is the same squared card — there is no variant or size to pick.
The only visual axis is type, which pulls a color from the --sapa-* tokens
and uses it twice: a soft wash with a hairline of the same hue on the icon tile,
and a solid 3px bar along the card's bottom edge — either the auto-dismiss
countdown or, when you pass progress, the determinate fill.
<Toaster> takes position and expand.
sapa/
├── registry.json # source of truth — every item (React + Vue)
├── registry/
│ ├── theme/globals.css # shared design tokens (framework-agnostic)
│ ├── react/ # .tsx toast: store + <Toaster/> + <Toast/> + examples
│ └── vue/ # .vue toast: reactive store + Toaster/Toast + examples
├── scripts/build-registry.mjs # inlines files → apps/registry/public/r/*.json
└── apps/
├── registry/ # Next.js showcase (live React preview, code + CLI copy)
└── vue-sandbox/ # Vite + Vue app to verify the .vue items render
pnpm install
pnpm dev # Next.js showcase → http://localhost:3000 (auto-runs registry:build)
pnpm sandbox # Vue sandbox → http://localhost:5174
pnpm registry:build # regenerate apps/registry/public/r/*.jsonEvery release and the files it touched: CHANGELOG.md · sapa-registry.vercel.app/changelog
Sapa is copy-paste — when you update, re-copy the files each entry names.
MIT © Muhamad Putra Satria
