Dialog and Lightbox overlays
Kumiko provides two overlay primitives for different interactions:
Dialogconfirms or cancels an action. It can also host a compact form.Lightboxshows an image at full size and has no confirm footer.
The React implementations use the shared modal shell for focus handling,
Escape, and backdrop clicks. Apex marketing pages use a separate native
<dialog> for .shot-frame screenshots.
Dialog
Section titled “Dialog”Keep open in the component that decides when the dialog appears. Pass
onOpenChange so cancel and close events update that state. onConfirm may be
async; the primitive shows its loading state and closes after the promise
resolves.
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";import { type ReactNode, useState } from "react";import { DemoPage, DemoSection } from "../components/page";
export function DialogDemo(): ReactNode { const { Button, Dialog, Lightbox, Text } = usePrimitives(); const [defaultOpen, setDefaultOpen] = useState(false); const [dangerOpen, setDangerOpen] = useState(false); const [asyncOpen, setAsyncOpen] = useState(false); const [lightboxOpen, setLightboxOpen] = useState(false); const [confirmedAction, setConfirmedAction] = useState<string>("(noch nichts bestätigt)");
return ( <DemoPage title="Dialog & Lightbox" description="Dialog für Bestätigungen; Lightbox für Vollbild-Bildvorschau. Beide teilen dieselbe Radix-Overlay-Shell (Focus-Trap, Esc, Backdrop-Click)." > <DemoSection title="Default-Variant"> <div className="flex flex-wrap items-center gap-2"> <Button onClick={() => setDefaultOpen(true)}>Dialog öffnen</Button> <Text variant="small">Letzte Aktion: {confirmedAction}</Text> </div> <Dialog open={defaultOpen} onOpenChange={setDefaultOpen} title="Bist du dir sicher?" description="Diese Aktion sendet eine E-Mail an alle Kunden. Bitte vorher prüfen." onConfirm={() => setConfirmedAction("Default-Confirm")} testId="dialog-default" /> </DemoSection>
<DemoSection title="Danger-Variant"> <Button variant="danger" onClick={() => setDangerOpen(true)}> Eintrag löschen… </Button> <Dialog open={dangerOpen} onOpenChange={setDangerOpen} title="Wirklich löschen?" description="Der Datensatz wird unwiderruflich entfernt. Diese Aktion kann nicht rückgängig gemacht werden." confirmLabel="Löschen" variant="danger" onConfirm={() => setConfirmedAction("Danger-Confirm (delete)")} testId="dialog-danger" /> </DemoSection>
<DemoSection title="Async onConfirm (Spinner)"> <Button onClick={() => setAsyncOpen(true)}>Async-Dialog öffnen</Button> <Dialog open={asyncOpen} onOpenChange={setAsyncOpen} title="Speichern dauert kurz" description="onConfirm ist async (1.5s). Confirm-Button zeigt während der Promise-Resolution einen Spinner; Dialog schließt automatisch danach." onConfirm={async () => { await new Promise((r) => setTimeout(r, 1500)); setConfirmedAction("Async-Confirm (1.5s)"); }} testId="dialog-async" /> </DemoSection>
<DemoSection title="Lightbox"> {/* kumiko-lint-ignore primitives-discipline clickable image thumbnail (lightbox trigger) — not a text button, <Button> cannot wrap a full-bleed <img> */} <button type="button" onClick={() => setLightboxOpen(true)} className="cursor-zoom-in overflow-hidden rounded-lg border border-border shadow-sm" data-testid="lightbox-trigger" > <img src="/screenshots/hero-app.png" alt="Tasklane planning board — click to enlarge" className="block h-auto w-full max-w-md" /> </button> <Text variant="small">Klick auf das Bild öffnet die Vollbild-Vorschau.</Text> <Lightbox open={lightboxOpen} onOpenChange={setLightboxOpen} src="/screenshots/hero-app.png" alt="Tasklane planning board — click to enlarge" testId="lightbox-demo" /> </DemoSection> </DemoPage> );}The runnable example includes a neutral confirmation, a destructive confirmation, an async confirmation, and a Lightbox trigger. Try it in the Showcase under Primitives → Dialog & Lightbox (port 4175).
Lightbox
Section titled “Lightbox”Pass the image src and accessible alt text. The app owns the thumbnail and
its open state:
const [open, setOpen] = useState(false);
<button type="button" onClick={() => setOpen(true)}> <img src="/screenshots/apex/landing.png" alt="Apex landing page" /></button><Lightbox open={open} onOpenChange={setOpen} src="/screenshots/apex/landing.png" alt="Apex landing page"/>The Showcase screenshot is stored at
samples/apps/showcase/screenshots/lightbox.png. Regenerate it with:
cd samples/apps/showcasebun run screenshotApex screenshots
Section titled “Apex screenshots”renderApexPage adds click-to-enlarge behavior to hero and showcase sections
that use .shot-frame. This path does not use React or the Lightbox primitive.
The runnable Apex example stores its screenshot at
samples/recipes/apex-landing/screenshots/lightbox.png.
Which primitive should you use?
Section titled “Which primitive should you use?”| Need | Primitive |
|---|---|
| Confirm before delete or another destructive action | Dialog with variant="danger" |
| Confirm a neutral or async action | Dialog |
| Show a product screenshot at full size | Lightbox in a React app |
| Put an existing form with its own buttons into an overlay | Modal |
Enlarge an Apex .shot-frame screenshot | Apex’s built-in lightbox |