- Components
Dialog
A modal window that traps focus, closes on Escape and gives focus back. Built on Radix Dialog.
npx shadcn add @klarhimmel/dialog @klarhimmel/buttonWhen to use
Use for
A short task or a decision that needs full attention: rename, confirm, delete.
Not for
Long forms or reading. Use a page. Not for hints. Use a Popover.
Pairs with
Button, Field, DialogClose.
Preview
dialog.tsx
import { Theme } from '@/components/klarhimmel/theme/style';
import { Button } from '@/components/klarhimmel/ui/button';
import { Dialog, DialogClose } from '@/components/klarhimmel/ui/dialog';
export default function Example() {
return (
<Theme theme="brand">
<Dialog
title="Rename the room"
trigger={<Button>Rename</Button>}
footer={<DialogClose><Button>Done</Button></DialogClose>}
>
…
</Dialog>
</Theme>
);
}Install
Import
import { Dialog, DialogClose } from '@/components/klarhimmel/ui/dialog';- Lands in
- components/klarhimmel/ui/dialog.tsx, plus cn() and the theme item if they are missing. Import theme.css after Tailwind.
- Render in
- Render it inside <Theme>, once near the root of your app. Tokens and fonts are scoped to it, so outside it the component is unstyled.
- Limit
- Overlays render into the Theme root with position: fixed. A transform on an ancestor re-parents fixed (a dialog centres in the wrapper) and overflow: hidden clips menus. Render <Theme> at the app root, outside any transformed wrapper.
Examples
Destructive
Nothing deleted yet.
import { useState } from 'react'; import { Button } from '@/components/klarhimmel/ui/button'; import { Dialog, DialogClose } from '@/components/klarhimmel/ui/dialog'; export default function DialogDestructive() { const [open, setOpen] = useState(false); const [done, setDone] = useState(false); return ( <div className="flex flex-col items-center gap-3"> <Button variant="secondary" onClick={() => setOpen(true)}>Delete the Garden room</Button> <p role="status" className="text-kh-small text-kh-muted">{done ? 'The Garden room was deleted.' : 'Nothing deleted yet.'}</p> <Dialog alert open={open} onOpenChange={setOpen} title="Delete the Garden room?" description="Its 14 upcoming bookings are cancelled and the guests are told. This cannot be undone." footer={ <> <DialogClose><Button variant="secondary">Keep the room</Button></DialogClose> <Button onClick={() => { setDone(true); setOpen(false); }}>Delete room</Button> </> } /> </div> ); }Scroll
import { Button } from '@/components/klarhimmel/ui/button'; import { Dialog, DialogClose } from '@/components/klarhimmel/ui/dialog'; export default function DialogScroll() { return ( <div className="flex items-start justify-center"> <Dialog title="House rules" description="Long content scrolls inside the dialog; the title and buttons stay in view." trigger={<Button variant="secondary">Read the house rules</Button>} footer={<DialogClose><Button>I understand</Button></DialogClose>} > {Array.from({ length: 10 }, (_, i) => ( <p key={i}> Rule {i + 1}. Quiet hours start at 22:00 in the garden and in the corridors, so that every guest at Exempelhotellet can rest. </p> ))} </Dialog> </div> ); }
Anatomy
The parts and how they nest.
Dialog
├─ trigger (your Button)
├─ title + description
├─ children (the body, scrolls)
└─ footer
└─ DialogClose (wraps a Button)DialogOwns open state and the overlay. Controlled with open, or opened by trigger.title / descriptionProps, not parts. The title names the dialog for assistive tech.footerButtons pinned under the scrolling body.DialogCloseWraps a footer Button so it closes the dialog.
API reference
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | - | Required. Names the dialog. |
description | string | - | One sentence under the title, read out with it. |
trigger | ReactNode | - | The element that opens it; focus returns to it. Omit for a controlled dialog. |
open / defaultOpen / onOpenChange | boolean / (open: boolean) => void | - | Controlled or uncontrolled state. |
footer | ReactNode | - | Buttons, pinned under the scrolling body. Wrap closing buttons in DialogClose. |
alert | boolean | false | role=alertdialog for a decision that cannot be missed: a click outside does not close it. |
className | string | - | Extra classes, merged after the component's own. |
Accessibility
| Key | Does |
|---|---|
Tab / Shift+Tab | Cycles inside the dialog; the page behind is unreachable |
Escape | Closes and returns focus to the trigger |
Enter / Space | Activates the focused button |
With alert set, a click outside does not close it; Escape still does.
Accessibility