# Modal (React Native)

> Centre, drawer, full-screen and bottom-sheet modals with close guards.

GramproKit for React Native · Overlays · Beta · Source: https://kit.gramproindia.com/native/docs/modal

> **Beta:** React Native support is in beta. APIs may change or break between prereleases (npm tag `native`).

_Interactive demo:_ [open the live example](https://kit.gramproindia.com/native/docs/modal). The example's source:

```tsx
import { useState } from "react";
import { Text, View } from "react-native";
import { Button } from "@/component-lib/button";
import { Modal } from "@/component-lib/modal";
import type { ModalPlacement, ModalSize } from "@/component-lib/modal";
import { useKitTheme } from "@/component-lib/shared/native";
import { Row, Section } from "./layout"; // Section and Row: page layout for the demo

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export function ModalDemo() {
  const { tokens } = useKitTheme();
  const [placement, setPlacement] = useState<ModalPlacement>("center");
  const [size, setSize] = useState<ModalSize>("md");
  const [open, setOpen] = useState(false);
  const [guarded, setGuarded] = useState(false);
  const [lastReason, setLastReason] = useState("—");

  const show = (p: ModalPlacement, s: ModalSize = "md") => {
    setPlacement(p);
    setSize(s);
    setOpen(true);
  };

  const body = (
    <View style={{ gap: 10 }}>
      <Text style={{ color: tokens.fg, fontSize: tokens.fontSize, lineHeight: tokens.fontSize * 1.45 }}>
        Close with ×, the backdrop, or Android back. The body scrolls when it is long; the footer stays put.
      </Text>
      {Array.from({ length: placement === "right" || placement === "left" ? 4 : 2 }, (_, i) => (
        <Text key={i} style={{ color: tokens.muted, fontSize: tokens.fontSize }}>
          Line {i + 1}: invoice #{1040 + i}, ₹{(12500 * (i + 1)).toLocaleString("en-IN")}
        </Text>
      ))}
    </View>
  );

  return (
    <>
      <Section title="Modal · placements">
        <Row>
          <Button variant="outline" onPress={() => show("center")}>
            Center
          </Button>
          <Button variant="outline" onPress={() => show("top", "sm")}>
            Top · sm
          </Button>
          <Button variant="outline" onPress={() => show("bottom")}>
            Bottom sheet
          </Button>
        </Row>
        <Row>
          <Button variant="outline" onPress={() => show("left")}>
            Left drawer
          </Button>
          <Button variant="outline" onPress={() => show("right")}>
            Right drawer
          </Button>
          <Button variant="outline" onPress={() => show("center", "full")}>
            Full screen
          </Button>
        </Row>
        <Text style={{ color: tokens.muted }}>Last close reason: {lastReason}</Text>
      </Section>

      <Section title="Modal · onBeforeClose">
        <Button variant="secondary" onPress={() => setGuarded(true)}>
          Edit with unsaved changes
        </Button>
      </Section>

      <Modal
        open={open}
        onOpenChange={(next, reason) => {
          setOpen(next);
          if (!next) setLastReason(reason ?? "—");
        }}
        placement={placement}
        size={size}
        title="Invoice #1042"
        description="Sent to Gram Panchayat, Kottayam"
        footer={({ close }) => (
          <>
            <Button variant="outline" onPress={close}>
              Cancel
            </Button>
            <Button onPress={() => wait(800).then(close)}>Mark paid</Button>
          </>
        )}
      >
        {body}
      </Modal>

      <Modal
        open={guarded}
        onOpenChange={setGuarded}
        title="Edit customer"
        closeOnBackdrop={false}
        onBeforeClose={(reason) => reason === "api"}
        footer={({ close }) => <Button onPress={close}>Save and close</Button>}
      >
        <Text style={{ color: tokens.fg, fontSize: tokens.fontSize, lineHeight: tokens.fontSize * 1.45 }}>
          The backdrop does nothing and × or back is refused by onBeforeClose: only "Save and close" closes it.
        </Text>
      </Modal>
    </>
  );
}
```

## Install

```bash
npx @grampro/kit@native add Modal
```

## Usage

```tsx
import { Modal } from "@/component-lib/modal";

<Modal open={open} onOpenChange={setOpen} title="Edit customer" placement="bottom">
  <CustomerForm />
</Modal>
```

## Props

### ModalProps

| Prop | Type | Description |
|---|---|---|
| `open` | `boolean` | Controlled open state. |
| `defaultOpen` | `boolean` | Open to begin with, when you are not controlling `open`. |
| `onOpenChange` | `(open: boolean, reason?: CloseReason) => void` | `reason` says what closed it; it is undefined when opening. Android back reports `escape`. |
| `title` | `ReactNode` |   |
| `description` | `ReactNode` | A line under the title. |
| `children` | `Renderable` |   |
| `footer` | `Renderable` | The actions row, pinned below the scrolling content. |
| `size` | `ModalSize` | Width for dialogs; for drawers, the width (left/right). Default `md`. |
| `placement` | `ModalPlacement` | Default `center`. `bottom` is a bottom sheet with a drag handle. |
| `closeButton` | `boolean` | Show the × button. Default true. |
| `closeOnEscape` | `boolean` | Android back closes. Named after the web's Escape key, which it stands in for. Default true. |
| `closeOnBackdrop` | `boolean` | Default true. |
| `onBeforeClose` | `(reason: CloseReason) => boolean \| Promise<boolean>` | Return `false` (or a promise of it) to keep the modal open, e.g. for unsaved changes. |
| `id` | `string` |   |
| `style` | `StyleProp<ViewStyle> \| ((state: ModalStyleState) => StyleProp<ViewStyle>)` |   |
| `styles` | `Partial<{ root: StyleProp<ViewStyle>; header: StyleProp<ViewStyle>; title: StyleProp<Te…` |   |
| `localeText` | `Partial<ModalLocaleText>` |   |

## Native and web

- Android back closes with reason `escape` and follows `closeOnEscape`.
- Web version: [Modal on the web](https://kit.gramproindia.com/modal).
