Skip to content

Dialog

Dialog draws a panel and a scrim. The behaviour comes from @hozo/patterns. Open it, then press Escape or one of its buttons:

Delete the project?

Every build and report goes with it. This cannot be undone.

DialogDemo.tsx
import { Heading, Paragraph } from '@hozo/core'
import { Button, Dialog, Stack } from '@hozo/ui'
import { useState } from 'react'
export function DialogDemo() {
const [open, setOpen] = useState(false)
return (
<>
<Button tone="danger" onPress={() => setOpen(true)}>
Delete project
</Button>
<Dialog open={open} onClose={() => setOpen(false)} accessibilityLabel="Delete the project">
<Stack gap="normal">
<Heading level={3} className="text-lg font-bold text-hozo-text">
Delete the project?
</Heading>
<Paragraph className="text-sm text-hozo-text-body">
Every build and report goes with it. This cannot be undone.
</Paragraph>
<Stack direction="row" gap="tight">
<Button tone="danger" onPress={() => setOpen(false)}>
Delete
</Button>
<Button tone="quiet" onPress={() => setOpen(false)}>
Keep it
</Button>
</Stack>
</Stack>
</Dialog>
</>
)
}

On the Web it is a real <dialog> opened with showModal(), so the browser supplies what a modal needs:

  • the rest of the page becomes inert
  • focus stays inside
  • Escape closes it, through the cancel event
  • focus goes back to the button that opened it

The scrim is the dialog’s ::backdrop, so there is no overlay element and no z-index to manage.

  • open drives it, and onClose is called when the browser asks to close it (Escape). Keep open in your state and set it to false there.
  • accessibilityLabel is the dialog’s accessible name. A screen reader announces it when the dialog opens.
  • There is no close button. A dismiss control needs a name and a place in the reading order, so it is yours to put in the children, as the example does. Escape works without one.
  • On React Native it is a Modal. Pass restoreFocusTo with a ref to the opener so TalkBack and VoiceOver land back on it, since React Native cannot report what had focus.