Knit UI
GitHub

Usage

Modal is always controlled: pass opened and an onClose that flips it back. Nothing opens a modal implicitly, so there is never a hidden source of truth for whether it is on screen.

import { Button, Modal } from "@knitui/components";
import { useDisclosure } from "@knitui/hooks";

function DeleteAccount() {
  const [opened, { open, close }] = useDisclosure(false);

  return (
    <>
      <Button onPress={open}>Delete account</Button>

      <Modal opened={opened} onClose={close} title="Delete account?">
        This cannot be undone.
      </Modal>
    </>
  );
}

useDisclosure is the usual companion — it is the opened / open / close / toggle tuple this pattern always needs.

For more structure, compose the parts directly: Modal.Header, Modal.Title, Modal.Body, Modal.CloseButton. The body scrolls on its own when the content is taller than the viewport, so the header stays pinned; withHeaderDivider draws a rule between them once the body has scrolled.

size sets the panel width, centered centres it vertically, and fullScreen expands it to fill the viewport — useful as a responsive escape hatch on phones.

When to use something else

  • A panel sliding in from an edge → Drawer
  • A sheet the user drags up from the bottom on native → Sheet
  • A small transient layer anchored to a trigger → Popover

Examples

Trigger

A controlled trigger — press the button to open, close button or backdrop to dismiss.

Loading example…

TriggerSourceStorybook

Sizes

All seven content widths shown via a controlled trigger for each size.

Loading example…

SizesSourceStorybook

Shadows

Each shadow elevation step (xxs–xxl) on the content panel.

Loading example…

ShadowsSourceStorybook

Centered

Vertically centred in the viewport — useful for confirmation dialogs.

Loading example…

CenteredSourceStorybook

Full Screen

Full-screen mode — no radius, no padding offset, fills the entire viewport.

Loading example…

Full ScreenSourceStorybook

Without Close Button

Close button hidden — the caller is responsible for dismissal.

Loading example…

Without Close ButtonSourceStorybook

Custom Close Label

Custom aria-label on the close button for screen-reader users.

Loading example…

Custom Close LabelSourceStorybook

Scrollable Body

Long content scrolls inside the body while the header stays pinned.

Loading example…

Scrollable BodySourceStorybook

Compound API

Compound API — compose Modal.Header, Modal.Title, and Modal.Body directly.

Loading example…

Compound APISourceStorybook

Styles

Per-slot styles targets individual parts — here the overlay, content, and title.

Loading example…

StylesSourceStorybook

With Header Divider

A hairline divider separates the pinned header from a scrolling body.

Loading example…

With Header DividerSourceStorybook

Custom Padding

Roomier padding on the header and body for a more spacious layout.

Loading example…

Custom PaddingSourceStorybook

Custom Close Button

The close control is fully customisable via closeButtonProps — here a larger, rounded button.

Loading example…

Custom Close ButtonSourceStorybook

No Overlay

No backdrop — the modal floats over the page without dimming it.

Loading example…

No OverlaySourceStorybook

Non Dismissable

A non-dismissable dialog — escape and backdrop clicks are disabled; only an explicit action closes it.

Loading example…

Non DismissableSourceStorybook

With Rich Content

Rich body content — a confirmation dialog with actions.

Loading example…

With Rich ContentSourceStorybook

Drag To Dismiss

Drag-to-dismiss (dragToDismiss) — flick the panel up to close it, the same reanimated + gesture mechanic as @knitui/sheet. The CSS enter/exit motion is unchanged; this only adds the interactive drag. Release past ~30% of the panel height (or an upward fling) dismisses; a short drag springs back. A stationary tap on the backdrop still dismisses.

Loading example…

Drag To DismissSourceStorybook

Props

PropTypeDefaultDescription
onClose required() => void—Called when the overlay requests to close (Escape, overlay click, close button).
opened requiredbooleanfalseControlled opened state.
animationfalse | MotionPresetName | MotionPreset | undefineda subtle drop-in fade. Reduced motion is always honoured. Exit animates (the panel and scrim fade out before unmounting) unless `keepMounted` is set.Enter/exit animation — a motion preset name, an inline {@link MotionPreset}, or `false` to disable.
animationConfigSpringConfig | undefined—Spring config for the drag settle (reanimated `withSpring`).
aria-describedbystring | undefined—Additional content description id(s).
aria-labelstring | undefined"Modal"Accessible label used when no visible title is provided.
aria-labelledbystring | undefined—Explicit label id used when composing a custom title.
centeredboolean | undefinedfalseCenter the content vertically inside the viewport.
childrenReactNodeModal body content goes here.Modal body content.
closeButtonPropsCloseButtonProps | undefined—Props forwarded to the header `CloseButton` — customise its `aria-label`, `icon`, `iconSize`, `size`, `variant`, `radius`, etc. A custom `onPress` overrides the default `onClose` wiring.
closeOnClickOutsideboolean | undefinedtrueClose when the user clicks the overlay scrim.
closeOnEscapeboolean | undefinedtrueClose when the user presses Escape (web).
disabled systemboolean | undefined——
dragThresholdnumber | undefined0.3Distance dragged, as a fraction of the panel height, past which releasing dismisses (a downward fling always dismisses).
dragToDismissboolean | undefinedfalseAllow dragging the panel down to dismiss (reanimated + gesture, the same mechanic as `@knitui/sheet`). The CSS enter/exit motion is unchanged; this only adds the interactive drag-to-close.
durationnumber | undefined—Animation speed in ms — overrides the preset's duration while keeping its easing. Ignored when `animation` is `false` or reduced motion is on.
fullScreenboolean | undefinedfalseExpand the modal to fill the entire screen.
keepMountedboolean | undefinedfalseKeep mounted while closed.
lockScrollboolean | undefinedtrueLock body scroll while opened (web).
overlayAnimationfalse | MotionPresetName | MotionPreset | undefined"fade". Reduced motion is always honoured. The content frame's own animation is supplied separately by the consumer (`Modal`/`Drawer`) on its content frame; pass `false` here to keep the scrim in lockstep when the consumer disables that.Enter/exit animation for the overlay scrim — a motion preset name, an inline {@link MotionPreset}, or `false` to disable.
overlayPropsOverlayProps | undefined—Props forwarded to the underlying `Overlay` scrim.
paddingnumber | SpaceTokens | Animated.AnimatedNode | "safe" | null | undefined"$md"Inner spacing for the header and body.
radius systemstring | number | undefined"md"Content corner radius.
returnFocusboolean | undefinedtrueReturn focus to the previously-focused element on close (web).
shadow system"xs" | "sm" | "md" | "lg" | "xl" | "xxs" | "xxl" | undefined
xxs · xs · sm · md · lg · xl · xxl
"xl"Drop-shadow elevation of the content panel (xxs–xxl).
size systemstring | number | undefined
xxs · xs · sm · md · lg · xl · xxl
"md"Content width — key (xxs–xxl), px number, or CSS string.
styles systemModalStyles | undefined—Uniform per-slot style passthrough. See {@link ModalStyles}.
theme systemThemeName | null | undefined—Applies a theme to this element
titleReactNodeModal titleModal title rendered in the header.
trapFocusboolean | undefinedtrueTrap keyboard focus while opened (web).
withCloseButtonboolean | undefinedtrueShow the close button in the header.
withHeaderDividerboolean | undefinedfalseRender a hairline divider under the header.
withinPortalboolean | undefinedtrueRender inside a `Portal`.
withOverlayboolean | undefinedtrueRender a dimmed backdrop behind the modal.
xOffsetnumber | SpaceTokens | Animated.AnimatedNode | "safe" | null | undefined"$xl"Horizontal gap from viewport edges.
yOffsetnumber | SpaceTokens | Animated.AnimatedNode | "safe" | null | undefined"$xl" (a cross-platform theme token; Mantine defaults to the web-only `5dvh`).Vertical gap from viewport edges.
zIndexnumber | undefined200Root `z-index`.

Style slots

Every part of Modal can be styled through the styles prop. Explicit props on the component always win over slot styles.

SlotTargets
bodyProps spread onto the body (.Body).
closeButtonProps spread onto the close button (under closeButtonProps).
contentProps spread onto the content frame (.Content).
headerProps spread onto the header (.Header).
overlayProps spread onto the Overlay scrim (under overlayProps).
titleProps spread onto the title (.Title).
Accessibility & DOM props (2)
PropType
keyKey | null | undefined
refRef<TamaguiElement> | undefined

Plus 493 inherited style props from Box — the full Tamagui/React Native style surface, including token shorthands like p, mx, bg and c. See Tokens for the scales they accept, or the full list.

Accessibility

The panel renders with role="dialog" and aria-modal, so assistive technology treats the rest of the page as inert while it is open.

Naming happens automatically when you pass title — it renders into the header and becomes the dialog's aria-labelledby target. If you build a custom header instead, supply aria-labelledby (pointing at your own heading) or aria-label yourself; a dialog with no accessible name is announced only as "dialog".

KeyAction
EscClose, unless closeOnEscape={false}
Tab / Shift+TabCycle focusable elements inside the panel, never behind it

Three defaults do the work, and all three are on unless you turn them off:

  • trapFocus keeps Tab inside the panel, via the shared useFocusTrap hook.
  • returnFocus moves focus back to whatever opened the modal when it closes, so the keyboard user is not dropped at the top of the document.
  • lockScroll stops the page behind the scrim from scrolling.

closeOnClickOutside is also on by default. Turn it off for a destructive confirmation, where an accidental click outside should not dismiss the question.

Edit this page on GitHub