nili/ui
All components

Modal

Overview
13 exports5 variant axes25 props4 files
import { Modal, StatusModal } from '@nili/ui';

This is the library’s own overview page, rendered live. Switch the language or the theme in the header and it follows.

Basic

Controlled by open and onOpenChange. It portals to the body, traps focus while open, and restores focus to the trigger on close.

import { Modal, Button } from "@nili/ui"

export function Example() {
  const [open, setOpen] = useState(false)

  return (
    <>
      <Button onClick={() => setOpen(true)}>Open</Button>

      <Modal
        open={open}
        onOpenChange={setOpen}
        title="Invite a teammate"
        description="They will get an email with a link to join."
        closeLabel="Close"
        footer={
          <>
            <Button appearance="stroke" onClick={() => setOpen(false)}>Cancel</Button>
            <Button onClick={() => setOpen(false)}>Send invite</Button>
          </>
        }
      >
        <TextInput label={t('invite.email')} placeholder="hello@nili.design" />
      </Modal>
    </>
  )
}

Sizes

Four panel widths. headerSize is separate — Figma's Medium (80) and Small (56) header.

import { Modal } from "@nili/ui"

export function Example() {
  return (
    <>
      <Modal size="sm" … />
      <Modal size="md" … />
      <Modal size="lg" … />
      <Modal size="xl" … />
    </>
  )
}

Header and alignment

icon is a free leading slot and wins over status. A vertical alignment centres the icon, title and text — the shape a confirmation wants.

import { Modal, Icon } from "@nili/ui"
import { RiDeleteBinLine } from "@remixicon/react"

export function Example() {
  return (
    <Modal
      open={open}
      onOpenChange={setOpen}
      alignment="vertical"
      status="error"
      icon={<Icon icon={RiDeleteBinLine} />}
      title="Delete this project?"
      description="This cannot be undone. Everything in it will be removed."
      stretchFooter
      footer={
        <>
          <Button appearance="stroke" onClick={close}>Cancel</Button>
          <Button color="error" onClick={close}>Delete</Button>
        </>
      }
    />
  )
}

Status Modal

The same modal with the status fixed: it picks the KeyIcon and its colour, so a “success” dialogue cannot arrive in red.

statuses
import { StatusModal, Button } from "@nili/ui"

export function Example() {
  return (
    <StatusModal
      open={open}
      onOpenChange={setOpen}
      status="success"
      alignment="vertical"
      title="Payment received"
      description="We have emailed you a receipt."
      stretchFooter
      footer={<Button onClick={close}>Done</Button>}
    />
  )
}

Controlling dismissal

closeOnOutsideClick and closeOnEscape default to on. Turn them off for a dialogue holding unsaved work — but then leave the close button, or there is no way out.

import { Modal } from "@nili/ui"

export function Example() {
  return (
    <Modal
      open={open}
      onOpenChange={setOpen}
      closeOnOutsideClick={false}
      closeOnEscape={false}
      showClose
      closeLabel="Close"
      title="Unsaved changes"
      …
    />
  )
}