

Cubitt components built on Base UI support **detached triggers**, allowing you to physically separate a component's trigger (like a button that opens a dialog) from the component's root element in your JSX structure while keeping them functionally connected.

## Why Detached Triggers? [#why-detached-triggers]

### Traditional Approach [#traditional-approach]

Normally, you nest the trigger inside the component's root element:

```tsx
<Dialog>
  <DialogTrigger>Open Dialog</DialogTrigger>
  <DialogContent>...</DialogContent>
</Dialog>
```

The `<DialogTrigger>` must be a child of `<Dialog>`. This works fine but becomes limiting when:

* Your trigger needs to be in a different part of the UI hierarchy
* You want several buttons in different places to open the same dialog
* The trigger and content are managed by different components

### Detached Approach [#detached-approach]

With detached triggers, you use a **handle** to connect components that are not parent-child in the DOM:

```tsx
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";

// 1. Create a handle
const myDialog = DialogPrimitive.createHandle();

// 2. Place trigger ANYWHERE in your app
<DialogTrigger handle={myDialog}>Open Dialog</DialogTrigger>

// 3. Place root ANYWHERE ELSE
<Dialog handle={myDialog}>
  <DialogContent>...</DialogContent>
</Dialog>
```

The `handle` acts as a "wireless connection" between the trigger and the root.

## How It Works [#how-it-works]

<Steps>
  <Step>
    ### Create a Handle [#create-a-handle]

    ```tsx
    import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";

    const myDialog = DialogPrimitive.createHandle();
    ```

    The `createHandle` function returns a special reference object that:

    * Manages the open/close state internally
    * Coordinates between triggers and the root component
    * Maintains accessibility relationships (ARIA attributes)
    * Handles focus management
  </Step>

  <Step>
    ### Link Components [#link-components]

    Both the trigger and root accept a `handle` prop:

    ```tsx
    <DialogTrigger handle={myDialog} />
    <Dialog handle={myDialog} />
    ```

    When you click the trigger, it signals through the handle to open the dialog, even if they're in completely different parts of the DOM tree.
  </Step>
</Steps>

## Supported Components [#supported-components]

The following Cubitt components support detached triggers:

<Cards columns="3">
  <Card title="Dialog" href="/primitives/dialog" />

  <Card title="AlertDialog" href="/primitives/alert-dialog" />

  <Card title="Sheet" href="/primitives/sheet" />

  <Card title="Popover" href="/primitives/popover" />

  <Card title="Menu" href="/primitives/menu" />

  <Card title="Select" href="/primitives/select" />
</Cards>

## Multiple Triggers [#multiple-triggers]

A single dialog can have multiple trigger elements. You can achieve this in two ways:

### Within Root [#within-root]

Place multiple `<DialogTrigger>` components inside the `<Dialog>` - no handle needed:

```tsx
<Dialog>
  <DialogTrigger render={<Button />}>Open from here</DialogTrigger>
  <DialogTrigger render={<Button variant="outline" />}>Or from here</DialogTrigger>
  <DialogTrigger render={<Button variant="ghost" />}>Or here too</DialogTrigger>

  <DialogContent>
    <DialogHeader>
      <DialogTitle>Example Dialog</DialogTitle>
    </DialogHeader>
  </DialogContent>
</Dialog>
```

### Detached [#detached]

Use a handle to connect multiple triggers placed anywhere in your app:

```tsx
const confirmDialog = DialogPrimitive.createHandle();

// Trigger in navbar
<DialogTrigger handle={confirmDialog} render={<Button />}>
  Delete from Header
</DialogTrigger>

// Trigger in sidebar
<DialogTrigger handle={confirmDialog} render={<Button />}>
  Delete from Sidebar
</DialogTrigger>

// Trigger in context menu
<DialogTrigger handle={confirmDialog} render={<Button />}>
  Delete from Context Menu
</DialogTrigger>

// Single dialog responds to all triggers
<Dialog handle={confirmDialog}>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Confirm Delete</DialogTitle>
    </DialogHeader>
    <DialogBody>Are you sure?</DialogBody>
  </DialogContent>
</Dialog>
```

## Programmatic Control [#programmatic-control]

Handles also provide methods for programmatic control:

```tsx
const myDialog = DialogPrimitive.createHandle();

// Open programmatically
<Button onClick={() => myDialog.open()}>
  Open via Method
</Button>

// Close programmatically
<Button onClick={() => myDialog.close()}>
  Close via Method
</Button>

<Dialog handle={myDialog}>
  <DialogContent>...</DialogContent>
</Dialog>
```

## Triggers with Payload [#triggers-with-payload]

The dialog can render different content depending on which trigger opened it. This is achieved by passing a `payload` to the trigger and using the function-as-a-child pattern.

<Steps>
  <Step>
    ### Create a Typed Handle [#create-a-typed-handle]

    Define your payload type when creating the handle:

    ```tsx
    import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";

    // Define the payload type
    const userDialog = DialogPrimitive.createHandle<{
      action: "view" | "edit" | "delete";
      userId: number;
      userName: string;
    }>();
    ```
  </Step>

  <Step>
    ### Add Payload to Each Trigger [#add-payload-to-each-trigger]

    ```tsx
    <DialogTrigger
      handle={userDialog}
      payload={{ action: "view", userId: 1, userName: "John" }}
      render={<Button variant="ghost" />}
    >
      View Profile
    </DialogTrigger>

    <DialogTrigger
      handle={userDialog}
      payload={{ action: "edit", userId: 1, userName: "John" }}
      render={<Button variant="outline" />}
    >
      Edit Profile
    </DialogTrigger>

    <DialogTrigger
      handle={userDialog}
      payload={{ action: "delete", userId: 1, userName: "John" }}
      render={<Button variant="destructive" />}
    >
      Delete User
    </DialogTrigger>
    ```
  </Step>

  <Step>
    ### Render Content Based on Payload [#render-content-based-on-payload]

    Use the function-as-a-child pattern to access the payload:

    ```tsx
    <Dialog handle={userDialog}>
      {({ payload }) => (
        <DialogContent>
          <DialogHeader>
            <DialogTitle>
              {payload?.action === "view" && `Viewing ${payload.userName}`}
              {payload?.action === "edit" && `Editing ${payload.userName}`}
              {payload?.action === "delete" && `Delete ${payload.userName}?`}
            </DialogTitle>
          </DialogHeader>
          <DialogBody>
            {payload?.action === "delete" ? (
              <p>This action cannot be undone.</p>
            ) : (
              <p>User ID: {payload?.userId}</p>
            )}
          </DialogBody>
          <DialogFooter>
            <DialogClose>Cancel</DialogClose>
            {payload?.action === "delete" ? (
              <DialogAction variant="destructive">Delete</DialogAction>
            ) : (
              <DialogAction>Confirm</DialogAction>
            )}
          </DialogFooter>
        </DialogContent>
      )}
    </Dialog>
    ```
  </Step>
</Steps>

## Combining with URL State [#combining-with-url-state]

Detached triggers work seamlessly with URL state management:

```tsx
const myDialog = DialogPrimitive.createHandle();

// Trigger anywhere
<DialogTrigger handle={myDialog} render={<Button />}>
  Open Settings
</DialogTrigger>

// Dialog with URL state
<Dialog handle={myDialog} paramName="settings">
  <DialogContent>...</DialogContent>
</Dialog>
```

The dialog will sync with `?settings=true` in the URL while still responding to the detached trigger.
