> ## Documentation Index
> Fetch the complete documentation index at: https://docs.launchmystore.io/llms.txt
> Use this file to discover all available pages before exploring further.

# React Components

> Declarative React wrappers and UI primitives shipped with @launchmystore/app-bridge-react

# React Components

`@launchmystore/app-bridge-react` ships **two kinds** of components:

1. **Chrome wrappers** — `<TitleBar />`, `<NavigationMenu />`,
   `<Loading />`. These render nothing visible. They render to the host
   chrome by calling the matching App Bridge action under the hood.
2. **In-iframe UI primitives** — `<AdminBlock>`, `<AdminAction>`,
   `<AdminPrintAction>`, layout/text/form primitives. These render
   inside your extension iframe and give you a consistent look without
   pulling in a separate design system.

All components require an `<AppBridgeProvider>` ancestor — see
[React Hooks](/app-bridge/react-hooks#appbridgeprovider).

## Chrome wrappers

These are declarative shells around their matching hooks. They return
`null` and re-render the host chrome whenever their props change. Use
them when you'd rather think in JSX than in imperative `.update()`
calls.

### `<TitleBar />`

Sets the admin page title, primary/secondary action buttons, and
breadcrumbs. Wraps [`useTitleBar`](/app-bridge/react-hooks#usetitlebar).

```tsx theme={null}
import { TitleBar } from '@launchmystore/app-bridge-react';

export default function ProductPage({ product }) {
  const [saving, setSaving] = useState(false);

  return (
    <>
      <TitleBar
        title={product.title}
        breadcrumbs={[{ label: 'Products', url: '/products' }]}
        primaryAction={{
          label: 'Save',
          loading: saving,
          onAction: async () => {
            setSaving(true);
            await saveProduct(product);
            setSaving(false);
          },
        }}
        secondaryActions={[
          { label: 'Duplicate', onAction: duplicate },
          { label: 'Delete', destructive: true, onAction: remove },
        ]}
      />
      <ProductForm product={product} />
    </>
  );
}
```

**Props (`TitleBarProps`)**

| Prop | Type | Description |
| - | - | - |
| `title` | `string` | Required. Shown as the page title in the host admin chrome. |
| `primaryAction` | `{ label, onAction?, disabled?, loading?, destructive? }` | Right-aligned filled button. |
| `secondaryActions` | `Array<{ label, onAction?, disabled?, destructive? }>` | Plain buttons left of the primary. |
| `breadcrumbs` | `Array<{ label, url? \| destination? }>` | Rendered as a chevron-separated trail before the title. |

Re-renders trigger an `update()` on the underlying action — flip
`loading: true` on the primary action to show a spinner without
re-creating the bar.

### `<NavigationMenu />`

Registers a left-rail navigation menu for the app's admin pages. Wraps
[`useNavigationMenu`](/app-bridge/react-hooks#usenavigationmenu).

```tsx theme={null}
import { NavigationMenu } from '@launchmystore/app-bridge-react';
import { useRouter } from 'next/router';

export default function AppLayout({ children }) {
  const router = useRouter();
  return (
    <>
      <NavigationMenu
        items={[
          { label: 'Dashboard', url: '/' },
          { label: 'Campaigns', url: '/campaigns', badge: 'New' },
          { label: 'Settings', url: '/settings' },
        ]}
        active={router.pathname}
        onNavigate={(url) => router.push(url)}
      />
      {children}
    </>
  );
}
```

**Props (`NavigationMenuProps`)**

| Prop | Type | Description |
| - | - | - |
| `items` | `Array<{ label, url, disabled?, icon?, badge? }>` | Required. Order = render order. |
| `active` | `string` | URL of the currently-active item — host highlights it. |
| `onNavigate` | `(url: string) => void` | Called when the user clicks an item. Use this to push to your router. |

The host swallows the click and fires this callback — you're
responsible for the in-iframe route change (e.g. `router.push(url)`).

### `<Loading />`

Toggles the global loading bar in the host chrome (the thin progress
strip at the top of the admin). Wraps
[`useLoading`](/app-bridge/react-hooks#useloading).

```tsx theme={null}
import { Loading } from '@launchmystore/app-bridge-react';

function ProductDetail({ id }) {
  const { data, isLoading } = useAppQuery(`/api/v1/products/${id}`);

  return (
    <>
      <Loading loading={isLoading} />
      {data ? <ProductView product={data} /> : null}
    </>
  );
}
```

**Props (`LoadingProps`)**

| Prop | Type | Default | Description |
| - | - | - | - |
| `loading` | `boolean` | `true` | When `true`, the host loading bar is shown. The bar hides on unmount automatically. |

### Contextual save bar

There's no `<ContextualSaveBar />` component — the save bar's
imperative API (`setSaveLoading`, `setDiscardLoading`) is a poor fit
for declarative JSX. Use the
[`useContextualSaveBar`](/app-bridge/react-hooks#usecontextualsavebar--usedirtystate)
or [`useDirtyState`](/app-bridge/react-hooks#usecontextualsavebar--usedirtystate)
hooks instead:

```tsx theme={null}
import { useDirtyState, TextField } from '@launchmystore/app-bridge-react';

function SettingsForm({ initial }) {
  const [form, setForm] = useState(initial);
  const { setDirty } = useDirtyState({
    onSave: () => saveSettings(form),
    onDiscard: () => setForm(initial),
  });

  return (
    <TextField
      label="Store name"
      value={form.name}
      onChange={(name) => {
        setForm((s) => ({ ...s, name }));
        setDirty(true);
      }}
    />
  );
}
```

## Admin extension containers

Use these as the outermost element of any extension iframe. They give
you a card / modal / print layout that matches the host admin look
without you having to import a design system.

### `<AdminBlock>`

Outer container for **block** extension targets (`product.details.block`,
`order.details.block`, etc.). Renders a white card with a title and
padding.

```tsx theme={null}
import { AdminBlock, BlockStack, Text, Badge } from '@launchmystore/app-bridge-react';

export default function Reviews() {
  return (
    <AdminBlock title="Reviews">
      <BlockStack gap="200">
        <Text variant="headingLg">4.8 / 5</Text>
        <Text tone="subdued">124 customer reviews</Text>
        <Badge tone="success">Verified app</Badge>
      </BlockStack>
    </AdminBlock>
  );
}
```

**Props (`AdminBlockProps`)** — `title?: string`, `padding?: number` (default 16), plus any `HTMLDivElement` attributes.

### `<AdminAction>`

Container for **modal-style** action extension targets
(`admin.order-details.action.render`, etc.). Renders a scrolling body
with a sticky footer containing the primary + secondary action
buttons. The footer handles button states (loading / destructive /
disabled) for you.

```tsx theme={null}
import { AdminAction, BlockStack, TextField } from '@launchmystore/app-bridge-react';

export default function RefundDialog({ order, onClose }) {
  const [amount, setAmount] = useState('');
  const [submitting, setSubmitting] = useState(false);

  return (
    <AdminAction
      primaryAction={{
        content: 'Issue refund',
        destructive: true,
        loading: submitting,
        onAction: async () => {
          setSubmitting(true);
          await issueRefund(order.id, amount);
          onClose();
        },
      }}
      secondaryActions={[
        { content: 'Cancel', onAction: onClose },
      ]}
    >
      <BlockStack gap="300">
        <TextField
          label="Refund amount"
          value={amount}
          onChange={setAmount}
        />
      </BlockStack>
    </AdminAction>
  );
}
```

**Props (`AdminActionProps`)**

* `primaryAction?: { content, onAction?, loading?, destructive?, disabled? }`
* `secondaryActions?: Array<{ content, onAction?, disabled? }>`
* Plus any `HTMLDivElement` attributes.

### `<AdminPrintAction>`

Container for **print** extension targets
(`admin.order-details.print.render`). Renders a full-page print
layout with `@media print` rules that reset margins and colors. Use
`usePrintReady()` from inside the tree to signal the host that the
content has finished loading and can be sent to the printer.

```tsx theme={null}
import { AdminPrintAction, usePrintReady, Heading, Text } from '@launchmystore/app-bridge-react';

export default function PackingSlip({ order }) {
  const { setReady } = usePrintReady();
  const { data } = useAppQuery(`/api/v1/orders/${order.id}`);
  useEffect(() => { if (data) setReady(true); }, [data]);

  if (!data) return null;
  return (
    <AdminPrintAction>
      <Heading level={1}>Packing slip</Heading>
      <Text>Order #{data.order_number}</Text>
    </AdminPrintAction>
  );
}
```

The host inspects `data-print-ready="true"` on the container and only
then calls the browser print dialog.

## Layout primitives

Token-driven flex layouts. Gaps use the spacing scale `"0" | "100" |
"200" | "300" | "400" | "500"` (= `0 | 4 | 8 | 12 | 16 | 24` px).

| Component | Purpose |
| - | - |
| `<BlockStack>` | Vertical flex column. `gap` defaults to `"200"`. |
| `<InlineStack>` | Horizontal flex row. `gap`, `align`, `blockAlign`. |
| `<Box>` | Generic `<div>` with token padding / background. |
| `<Divider>` | Thin horizontal rule using `tokens.color.border`. |

```tsx theme={null}
<BlockStack gap="300">
  <Text variant="headingMd">Order #1234</Text>
  <InlineStack gap="200" align="space-between">
    <Text>Total</Text>
    <Text fontWeight="semibold">$99.00</Text>
  </InlineStack>
  <Divider />
</BlockStack>
```

## Typography

| Component | Purpose |
| - | - |
| `<Text>` | Body / heading text. `variant` (`bodyXs`–`bodyLg`, `headingSm`–`headingXl`), `tone` (`base`, `subdued`, `critical`, `success`, `caution`), `fontWeight`, `as`. |
| `<Heading>` | `h1`–`h6` with `level: 1-6`. Wraps `<Text>` with sensible defaults. |
| `<Link>` | Anchor with token color + hover underline. Pass `external` for `target="_blank" rel="noreferrer"`. |

```tsx theme={null}
<Heading level={2}>Inventory</Heading>
<Text tone="subdued">Last synced 3 minutes ago.</Text>
<Link external href="https://launchmystore.io/docs">Read the docs</Link>
```

## Display

| Component | Purpose |
| - | - |
| `<Banner>` | Tone-coloured callout. `tone: 'info' \| 'warning' \| 'critical' \| 'success'`, `title`, `onDismiss`. |
| `<Badge>` | Pill label. `tone` (`base`, `info`, `success`, `warning`, `critical`, `attention`), `size: 'small' \| 'medium'`. |
| `<Image>` | `<img>` with rounded corners + lazy-loading default. |
| `<Icon>` | Inline SVG icon. Pass a `source` SVG path or use the small built-in set. |
| `<EmptyState>` | Centered "nothing here yet" block with optional `<Button>` action. |
| `<ProgressIndicator>` | Indeterminate circular spinner (no `value` prop). `size: 'small' \| 'medium' \| 'large'`, `color`. |
| `<Spinner>` | Alias of `<ProgressIndicator>`. |

```tsx theme={null}
<Banner tone="critical" title="Sync failed" onDismiss={dismiss}>
  Couldn't reach the upstream provider. We'll retry in 5 minutes.
</Banner>
```

## Form

Controlled inputs. All call `onChange(value)` with the next value
(no event object). All accept `label`, `helpText`, `error`.

| Component | Purpose |
| - | - |
| `<TextField>` | `<input>` / `<textarea>` (`multiline`). Supports `prefix` / `suffix` slots, `type: 'text' \| 'email' \| 'password' \| 'search' \| 'tel' \| 'url' \| 'number'`. |
| `<NumberField>` | `TextField` with `inputMode="numeric"` + numeric onChange. `min`, `max`, `step`. |
| `<MoneyField>` | `TextField` with currency prefix. `currency: 'USD'` (etc.). |
| `<DateField>` | `<input type="date">` returning ISO `YYYY-MM-DD`. |
| `<Select>` | Native `<select>`. `options: Array<{ label, value }>`. |
| `<Checkbox>` | Single boolean. |
| `<ChoiceList>` | Radio (single) or checkbox (`allowMultiple`) group. `choices: Array<{ label, value, helpText? }>`. |
| `<Button>` | `variant: 'primary' \| 'secondary' \| 'tertiary' \| 'plain'`, `tone: 'base' \| 'critical' \| 'success'`, `size`, `loading`. |
| `<Form>` | `<form>` that calls `onSubmit(e)` and prevents default. Useful for `Enter` to submit. |

```tsx theme={null}
import { Form, TextField, MoneyField, Select, Button, BlockStack } from '@launchmystore/app-bridge-react';

function CreateDiscount() {
  const [code, setCode] = useState('');
  const [amount, setAmount] = useState('10');
  const [kind, setKind] = useState('percentage');

  return (
    <Form onSubmit={() => createDiscount({ code, amount, kind })}>
      <BlockStack gap="300">
        <TextField
          label="Discount code"
          value={code}
          onChange={setCode}
          helpText="Customers enter this at checkout"
        />
        <Select
          label="Kind"
          value={kind}
          onChange={setKind}
          options={[
            { label: 'Percentage off', value: 'percentage' },
            { label: 'Fixed amount off', value: 'fixed' },
          ]}
        />
        {kind === 'percentage'
          ? <NumberField label="Percent off" value={amount} onChange={setAmount} min={0} max={100} />
          : <MoneyField label="Amount off" value={amount} onChange={setAmount} currency="USD" />}
        <Button variant="primary">Create</Button>
      </BlockStack>
    </Form>
  );
}
```

## Design tokens

All components read from a shared token set exported by
`@launchmystore/app-bridge-react`:

* `tokens.color.*` — base, surface, border, primary, critical, success,
  info, warning, plus `*Subdued` / `*Border` variants for banners.
* `tokens.font.*` — `family`, `size.{xs,sm,md,lg,xl}`, `weight.*`,
  `lineHeight.*`.
* `tokens.radius.*` — `sm` (4), `md` (6), `lg` (8).
* `tokens.shadow.*` — `sm` for cards.

You can override per-component via the standard `style` prop — the
token style is spread first, your `style` last.

## When to use components vs. hooks

| Goal | Use |
| - | - |
| Set the title bar declaratively | `<TitleBar />` |
| Toggle the title bar's `primaryAction.loading` | Either — the component re-renders. |
| Open a confirmation modal once on click | `useConfirmationModal` hook |
| Render a scrolling-with-sticky-footer action page | `<AdminAction>` |
| Show / hide the host loading bar | `<Loading loading={...} />` |
| Manage a contextual save bar tied to form state | `useDirtyState` hook |
| Open a resource picker imperatively | `useResourcePicker` / `useApi().picker` |

Both styles use the same wire protocol — pick whichever reads better
in the component you're writing.

## See Also

* [React Hooks](/app-bridge/react-hooks) — every hook the components
  wrap, plus the data-fetching / cart / lifecycle hooks that have no
  component equivalent.
* [Actions Reference](/app-bridge/actions) — raw payload shapes for
  every App Bridge action.
* [App Bridge Overview](/app-bridge/overview) — the iframe ↔ host
  postMessage contract underneath all of this.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.