Klarhimmel UI

Dialog

Stable

A modal window that traps focus, closes on Escape and gives focus back. Built on Radix Dialog.

npx shadcn add @klarhimmel/dialog @klarhimmel/button

When 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

PropTypeDefaultDescription
titlestring-Required. Names the dialog.
descriptionstring-One sentence under the title, read out with it.
triggerReactNode-The element that opens it; focus returns to it. Omit for a controlled dialog.
open / defaultOpen / onOpenChangeboolean / (open: boolean) => void-Controlled or uncontrolled state.
footerReactNode-Buttons, pinned under the scrolling body. Wrap closing buttons in DialogClose.
alertbooleanfalserole=alertdialog for a decision that cannot be missed: a click outside does not close it.
classNamestring-Extra classes, merged after the component's own.

Accessibility

KeyDoes
Tab / Shift+TabCycles inside the dialog; the page behind is unreachable
EscapeCloses and returns focus to the trigger
Enter / SpaceActivates the focused button

With alert set, a click outside does not close it; Escape still does.

Accessibility