Aksara UI

Dialog

A modal window for completing a short task or confirming a decision.

import { Dialog } from '@aksornproject/aksara-ui'

Basic dialog

Dialog is a namespace of parts. Dialog.Trigger opens the dialog and Dialog.Close closes it. Both are unstyled, so pass a library Button through render.

Dialog.Popup draws the scrim, centres the surface and keeps it 24px from the overlay edges. Put a Dialog.Header, a Dialog.Body and a Dialog.Footer inside it.

<Dialog.Root>
  <Dialog.Trigger render={<Button>Share report</Button>} />
  <Dialog.Popup size="md">
    <Dialog.Header title="Share report" subtitle="Term 1 science grades" />
    <Dialog.Body>
      <Typography className="text-color-text-primary">
        Parents of the 32 students in M.1/2 get an email with a link to
        their child’s report.
      </Typography>
    </Dialog.Body>
    <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-8">
      <Dialog.Close
        render={
          <Button variant="secondary-accent" size="lg">
            Cancel
          </Button>
        }
      />
      <Dialog.Close render={<Button size="lg">Share</Button>} />
    </Dialog.Footer>
  </Dialog.Popup>
</Dialog.Root>

Sizes

Choose sm, md, lg or xl with size. lg is the default. Each size adapts to the available width and leaves at least 24px at the edges. full fills the overlay with no corner radius.

Sizes follow the overlay's width. If you use container, they adapt to that element rather than the browser window.

{SIZES.map((size) => (
  <Dialog.Root key={size}>
    <Dialog.Trigger
      render={<Button variant="secondary-accent">{size}</Button>}
    />
    <Dialog.Popup size={size}>
      <Dialog.Header title={`A ${size} dialog`} />
      <Dialog.Body>
        <Typography className="text-color-text-primary">
          The width comes from the surface modal width token for {size}.
        </Typography>
      </Dialog.Body>
      <Dialog.Footer className="flex justify-end">
        <Dialog.Close render={<Button size="lg">Done</Button>} />
      </Dialog.Footer>
    </Dialog.Popup>
  </Dialog.Root>
))}

title names the dialog and clamps to two lines. subtitle adds a description below it. leadingIcon puts an icon before the title.

info adds a question button after the title. Its value is the tooltip content. actions sits between the title and the close button. extra adds a row under the title for badges or steps. showClose={false} removes the close button.

<Dialog.Root>
  <Dialog.Trigger
    render={<Button variant="secondary-accent">Class settings</Button>}
  />
  <Dialog.Popup size="md">
    <Dialog.Header
      title="Class settings"
      subtitle="M.1/2 science"
      leadingIcon={<IconInterfaceEssentialSetting />}
      info="Only teachers of this class can change these settings."
      actions={
        <Button variant="tertiary-accent" size="sm">
          Reset
        </Button>
      }
      extra={<Badge>32 students</Badge>}
    />
    <Dialog.Body>
      <Typography className="text-color-text-primary">
        Choose who can join and when homework is due.
      </Typography>
    </Dialog.Body>
  </Dialog.Popup>
</Dialog.Root>

The header and the footer stay put. The body takes the rest of the height and scrolls once the popup reaches the overlay height. You do not set a height. This demo caps the popup with max-h-[480px] to show the scroll.

padding on Dialog.Body is default (24px) or flush (none). Both keep the 16px gap. Use flush for rows that touch the edges.

Dialog.Footer is one styled div with the surface, the top divider and the 24 by 16 padding. It never reorders, groups or stacks its children. Arrange the buttons in JSX and className. The JSX order is the DOM order, the Tab order and the screen order.

<Dialog.Root>
  <Dialog.Trigger render={<Button>Filters</Button>} />
  <Dialog.Popup size="md" className="max-h-[480px]">
    <Dialog.Header
      title="Filters"
      subtitle="Grade"
      extra={<Badge>{`${picked.length} selected`}</Badge>}
    />
    <Dialog.Body padding="flush">
      {GRADES.map((grade) => (
        <div key={grade} className="px-spacing-24 py-spacing-12">
          <Checkbox
            label={grade}
            checked={picked.includes(grade)}
            onCheckedChange={(checked) =>
              setPicked((all) =>
                checked
                  ? [...all, grade]
                  : all.filter((entry) => entry !== grade),
              )
            }
          />
        </div>
      ))}
    </Dialog.Body>
    <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-12">
      <div className="flex min-w-spacing-160 flex-1 items-center">
        <Button
          variant="link-accent"
          size="lg"
          disabled={picked.length === 0}
          onClick={() => setPicked([])}
        >
          Clear all
        </Button>
      </div>
      <div className="flex items-center gap-spacing-8">
        <Dialog.Close
          render={
            <Button variant="secondary-accent" size="lg">
              Cancel
            </Button>
          }
        />
        <Dialog.Close render={<Button size="lg">Apply</Button>} />
      </div>
    </Dialog.Footer>
  </Dialog.Popup>
</Dialog.Root>

Alert dialog

Use Dialog.AlertRoot for a confirmation. It gives the popup role="alertdialog", is always modal and ignores presses outside. Only its buttons and Escape close it. Dialog.Trigger and the other parts work the same inside it.

<Dialog.AlertRoot>
  <Dialog.Trigger
    render={<Button color="destructive">Delete class</Button>}
  />
  <Dialog.Popup size="sm">
    <Dialog.Header title="Delete M.1/2?" />
    <Dialog.Body>
      <Typography className="text-color-text-primary">
        Students will no longer see this class or its homework.
      </Typography>
    </Dialog.Body>
    <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-8">
      <Dialog.Close
        render={
          <Button variant="secondary-accent" size="lg">
            Cancel
          </Button>
        }
      />
      <Dialog.Close
        render={
          <Button color="destructive" size="lg">
            Delete class
          </Button>
        }
      />
    </Dialog.Footer>
  </Dialog.Popup>
</Dialog.AlertRoot>

Controlled

Pass open and onOpenChange to Dialog.Root to manage the state in your app, for example to close after a save.

The popup takes focus when it opens. Pass initialFocus to start in a field instead.

Science M.1/2

<Typography variant="body-medium" className="text-color-text-primary">
  {name}
</Typography>
<Dialog.Root open={open} onOpenChange={openChange}>
  <Dialog.Trigger
    render={<Button variant="secondary-accent">Rename</Button>}
  />
  <Dialog.Popup size="sm" initialFocus={field}>
    <Dialog.Header title="Rename class" />
    <Dialog.Body>
      <TextInput
        ref={field}
        label="Class name"
        value={draft}
        onChange={(event) => setDraft(event.target.value)}
        onKeyDown={(event) => {
          if (event.key === 'Enter') save()
        }}
      />
    </Dialog.Body>
    <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-8">
      <Dialog.Close
        render={
          <Button variant="secondary-accent" size="lg">
            Cancel
          </Button>
        }
      />
      <Button size="lg" onClick={save}>
        Save
      </Button>
    </Dialog.Footer>
  </Dialog.Popup>
</Dialog.Root>

Container

container renders the overlay inside an element instead of <body>. The overlay fills the element and the width tokens follow its width. Give the element a non-static position. Dialogs nested in a contained dialog stay inside the same element.

Menus, tooltips and nested dialogs opened inside a dialog portal into its layer, so they paint above the popup. A nested dialog draws its own backdrop.

<div
  ref={stage}
  className="relative flex h-[360px] w-full max-w-[810px] items-center justify-center overflow-hidden rounded-radius-radius-8 bg-color-background-surface"
>
  <Dialog.Root>
    <Dialog.Trigger render={<Button>Leave lesson</Button>} />
    <Dialog.Popup size="sm" container={stage}>
      <Dialog.Header title="Leave the lesson?" />
      <Dialog.Body>
        <Typography className="text-color-text-primary">
          The lesson stops on the current slide.
        </Typography>
      </Dialog.Body>
      <Dialog.Footer className="flex flex-wrap items-center justify-end gap-spacing-8">
        <Dialog.Close
          render={
            <Button variant="secondary-accent" size="lg">
              Keep teaching
            </Button>
          }
        />
        <Dialog.Close render={<Button size="lg">Leave</Button>} />
      </Dialog.Footer>
    </Dialog.Popup>
  </Dialog.Root>
</div>

Accessibility

  • The header title labels the popup through aria-labelledby. The subtitle describes it through aria-describedby.
  • A popup without a Dialog.Header needs a Dialog.Title or an aria-label. Development builds log an error otherwise.
  • The popup takes focus when it opens, so the info tooltip does not cover the title for keyboard users. The next Tab reaches the info button. Focus returns to the trigger on close.
  • The close button is named "Close" and the info button "More information". Pass closeLabel and infoLabel to translate them.
  • Dialog.AlertRoot gives role="alertdialog". Use it for confirmations.

API

Dialog.Root

Prop

Type

Dialog.AlertRoot

Prop

Type

Dialog.Trigger

Prop

Type

Dialog.Close

Prop

Type

Dialog.Popup

Use className to customise the popup. Dialog.Popup does not accept render.

Prop

Type

Dialog.Header

Prop

Type

Dialog.Title

Prop

Type

Dialog.Description

Prop

Type

Dialog.Body

Prop

Type

Dialog.Footer

DialogFooterProps is the props of a div, with a forwarded ref and className merged last. It adds no props of its own.

Edit on GitHub

On this page