Overlays@knitui/components
Modal
Modal is an overlay dialog composed of Modal.Content, Modal.Header, Modal.Title, Modal.Body, and Modal.CloseButton. Pass opened and onClose to control visibility. size sets the content width, centered vertically centres the panel, and fullScreen expands it to fill the viewport. padding tunes the inner spacing, withHeaderDivider separates a pinned header from a scrolling body, and closeButtonProps fully customises the close control. Behaviour knobs (closeOnEscape, closeOnClickOutside, lockScroll, trapFocus, returnFocus, keepMounted) mirror Mantine. The body scrolls automatically when its content is taller than the viewport while the header stays pinned.
import { Modal } from "@knitui/components";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…
Sizes
All seven content widths shown via a controlled trigger for each size.
Loading example…
Shadows
Each shadow elevation step (xxs–xxl) on the content panel.
Loading example…
Centered
Vertically centred in the viewport — useful for confirmation dialogs.
Loading example…
Full Screen
Full-screen mode — no radius, no padding offset, fills the entire viewport.
Loading example…
Without Close Button
Close button hidden — the caller is responsible for dismissal.
Loading example…
Custom Close Label
Custom aria-label on the close button for screen-reader users.
Loading example…
Scrollable Body
Long content scrolls inside the body while the header stays pinned.
Loading example…
Compound API
Compound API — compose Modal.Header, Modal.Title, and Modal.Body directly.
Loading example…
Styles
Per-slot styles targets individual parts — here the overlay, content, and title.
Loading example…
With Header Divider
A hairline divider separates the pinned header from a scrolling body.
Loading example…
Custom Padding
Roomier padding on the header and body for a more spacious layout.
Loading example…
Custom Close Button
The close control is fully customisable via closeButtonProps — here a larger, rounded button.
Loading example…
No Overlay
No backdrop — the modal floats over the page without dimming it.
Loading example…
Non Dismissable
A non-dismissable dialog — escape and backdrop clicks are disabled; only an explicit action closes it.
Loading example…
With Rich Content
Rich body content — a confirmation dialog with actions.
Loading example…
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…
Props
| Prop | Type | Default | Description |
|---|---|---|---|
onClose required | () => void | — | Called when the overlay requests to close (Escape, overlay click, close button). |
opened required | boolean | false | Controlled opened state. |
animation | false | MotionPresetName | MotionPreset | undefined | a 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. |
animationConfig | SpringConfig | undefined | — | Spring config for the drag settle (reanimated `withSpring`). |
aria-describedby | string | undefined | — | Additional content description id(s). |
aria-label | string | undefined | "Modal" | Accessible label used when no visible title is provided. |
aria-labelledby | string | undefined | — | Explicit label id used when composing a custom title. |
centered | boolean | undefined | false | Center the content vertically inside the viewport. |
children | ReactNode | Modal body content goes here. | Modal body content. |
closeButtonProps | CloseButtonProps | 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. |
closeOnClickOutside | boolean | undefined | true | Close when the user clicks the overlay scrim. |
closeOnEscape | boolean | undefined | true | Close when the user presses Escape (web). |
disabled system | boolean | undefined | — | — |
dragThreshold | number | undefined | 0.3 | Distance dragged, as a fraction of the panel height, past which releasing dismisses (a downward fling always dismisses). |
dragToDismiss | boolean | undefined | false | Allow 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. |
duration | number | undefined | — | Animation speed in ms — overrides the preset's duration while keeping its easing. Ignored when `animation` is `false` or reduced motion is on. |
fullScreen | boolean | undefined | false | Expand the modal to fill the entire screen. |
keepMounted | boolean | undefined | false | Keep mounted while closed. |
lockScroll | boolean | undefined | true | Lock body scroll while opened (web). |
overlayAnimation | false | 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. |
overlayProps | OverlayProps | undefined | — | Props forwarded to the underlying `Overlay` scrim. |
padding | number | SpaceTokens | Animated.AnimatedNode | "safe" | null | undefined | "$md" | Inner spacing for the header and body. |
radius system | string | number | undefined | "md" | Content corner radius. |
returnFocus | boolean | undefined | true | Return focus to the previously-focused element on close (web). |
shadow system | "xs" | "sm" | "md" | "lg" | "xl" | "xxs" | "xxl" | undefinedxxs · xs · sm · md · lg · xl · xxl | "xl" | Drop-shadow elevation of the content panel (xxs–xxl). |
size system | string | number | undefinedxxs · xs · sm · md · lg · xl · xxl | "md" | Content width — key (xxs–xxl), px number, or CSS string. |
styles system | ModalStyles | undefined | — | Uniform per-slot style passthrough. See {@link ModalStyles}. |
theme system | ThemeName | null | undefined | — | Applies a theme to this element |
title | ReactNode | Modal title | Modal title rendered in the header. |
trapFocus | boolean | undefined | true | Trap keyboard focus while opened (web). |
withCloseButton | boolean | undefined | true | Show the close button in the header. |
withHeaderDivider | boolean | undefined | false | Render a hairline divider under the header. |
withinPortal | boolean | undefined | true | Render inside a `Portal`. |
withOverlay | boolean | undefined | true | Render a dimmed backdrop behind the modal. |
xOffset | number | SpaceTokens | Animated.AnimatedNode | "safe" | null | undefined | "$xl" | Horizontal gap from viewport edges. |
yOffset | number | SpaceTokens | Animated.AnimatedNode | "safe" | null | undefined | "$xl"
(a cross-platform theme token; Mantine defaults to the web-only `5dvh`). | Vertical gap from viewport edges. |
zIndex | number | undefined | 200 | Root `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.
| Slot | Targets |
|---|---|
body | Props spread onto the body (.Body). |
closeButton | Props spread onto the close button (under closeButtonProps). |
content | Props spread onto the content frame (.Content). |
header | Props spread onto the header (.Header). |
overlay | Props spread onto the Overlay scrim (under overlayProps). |
title | Props spread onto the title (.Title). |
Accessibility & DOM props (2)
| Prop | Type |
|---|---|
key | Key | null | undefined |
ref | Ref<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".
| Key | Action |
|---|---|
| Esc | Close, unless closeOnEscape={false} |
| Tab / Shift+Tab | Cycle focusable elements inside the panel, never behind it |
Three defaults do the work, and all three are on unless you turn them off:
trapFocuskeeps Tab inside the panel, via the shareduseFocusTraphook.returnFocusmoves focus back to whatever opened the modal when it closes, so the keyboard user is not dropped at the top of the document.lockScrollstops 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.