---
name: choosing-the-right-component
type: lifecycle
library: "@loke/ui"
library_version: "1.0.0-rc.1"
requires:
  - loke-ui
description: >
  Decision guide for selecting the correct @loke/ui primitive. Dialog vs AlertDialog,
  Popover vs Tooltip vs DropdownMenu, Select vs Popover+Command, Accordion vs Collapsible,
  Checkbox vs Switch. Import path rules: always @loke/ui/[component], never @loke/ui
  root, never @radix-ui paths. Failure modes: hallucinated components, native HTML fallback,
  skipping portals, verbose intermediary requirements, conciseness trap.
---

# Choosing the Right Component

## Import Rules

Every import must use the component subpath. There is no barrel export at `@loke/ui`.

```tsx
// CORRECT
import { Dialog, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogClose } from "@loke/ui/dialog";
import { Select, SelectTrigger, SelectContent, SelectViewport, SelectItem, SelectItemText } from "@loke/ui/select";

// WRONG
import { Dialog } from "@loke/ui";                  // no barrel export
import { Dialog } from "@radix-ui/react-dialog";    // wrong package — API is similar but package is different
import { DialogHeader } from "@loke/ui/dialog";     // hallucinated — does not exist
import { DialogFooter } from "@loke/ui/dialog";     // hallucinated — does not exist
```

## Decision Tables

### Dialog vs AlertDialog

| Condition | Use |
|---|---|
| Destructive action (delete, discard, overwrite) | `AlertDialog` |
| Need to auto-focus Cancel button for safety | `AlertDialog` |
| Need to prevent outside-click dismiss | `AlertDialog` |
| General modal with custom content | `Dialog` |
| Non-modal (side panel, drawer-style) | `Dialog` with `modal={false}` |

```tsx
// Destructive confirmation → AlertDialog
import { AlertDialog, AlertDialogTrigger, AlertDialogPortal, AlertDialogOverlay,
  AlertDialogContent, AlertDialogTitle, AlertDialogDescription,
  AlertDialogCancel, AlertDialogAction } from "@loke/ui/alert-dialog";

<AlertDialog>
  <AlertDialogTrigger>Delete account</AlertDialogTrigger>
  <AlertDialogPortal>
    <AlertDialogOverlay />
    <AlertDialogContent>
      <AlertDialogTitle>Delete account?</AlertDialogTitle>
      <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Delete</AlertDialogAction>
    </AlertDialogContent>
  </AlertDialogPortal>
</AlertDialog>
```

AlertDialog hardcodes `modal={true}` — passing `modal={false}` has no effect.

### Popover vs Tooltip

| Condition | Use |
|---|---|
| Interactive content (buttons, inputs, links) | `Popover` |
| Triggered by click | `Popover` |
| Need focus trapping inside | `Popover` with `modal` |
| Non-interactive text label only | `Tooltip` |
| Triggered by hover or focus only | `Tooltip` |
| Need `aria-describedby` semantics | `Tooltip` |

Tooltip closes on click/pointer-down and uses `aria-describedby`. It cannot contain interactive elements. Popover uses `role=dialog` and can trap focus.

```tsx
// Interactive content → Popover
import { Popover, PopoverTrigger, PopoverPortal, PopoverContent } from "@loke/ui/popover";

<Popover>
  <PopoverTrigger>Edit profile</PopoverTrigger>
  <PopoverPortal>
    <PopoverContent>
      <input placeholder="Display name" />
      <button>Save</button>
    </PopoverContent>
  </PopoverPortal>
</Popover>
```

### DropdownMenu vs Select

| Condition | Use |
|---|---|
| List of actions (navigate, delete, copy) | `DropdownMenu` |
| Needs `onSelect` per item | `DropdownMenu` |
| Picking a form value for submission | `Select` |
| Needs hidden native `<select>` for forms | `Select` |
| Needs `aria-labelledby` from a `<label>` | `Select` |

DropdownMenu items fire `onSelect` and close the menu. Select tracks a value and renders a hidden native `<select>` for form participation. They are not interchangeable.

### Select vs Popover + Command

| Condition | Use |
|---|---|
| Static option list known at render time | `Select` |
| Typeahead over a fixed list | `Select` |
| Searchable filter input | `Popover` + `Command` |
| Options loaded asynchronously | `Popover` + `Command` |
| Need fuzzy matching or custom scoring | `Popover` + `Command` |

Select assumes all items exist in the tree at open time — item-aligned positioning, `SelectItemText` portal, and typeahead all require this. For async or filterable lists:

```tsx
import { Popover, PopoverTrigger, PopoverPortal, PopoverContent } from "@loke/ui/popover";
import { Command, CommandInput, CommandList, CommandItem, CommandEmpty } from "@loke/ui/command";

<Popover>
  <PopoverTrigger>Select framework...</PopoverTrigger>
  <PopoverPortal>
    <PopoverContent>
      <Command>
        <CommandInput placeholder="Search..." />
        <CommandList>
          <CommandEmpty>No results.</CommandEmpty>
          <CommandItem value="react" onSelect={() => setValue("react")}>React</CommandItem>
          <CommandItem value="vue" onSelect={() => setValue("vue")}>Vue</CommandItem>
        </CommandList>
      </Command>
    </PopoverContent>
  </PopoverPortal>
</Popover>
```

### Accordion vs Collapsible

| Condition | Use |
|---|---|
| Single expandable section | `Collapsible` |
| Multiple independent expandable sections | `Accordion` with `type="multiple"` |
| Multiple sections, one open at a time | `Accordion` with `type="single"` |
| Need keyboard navigation across sections | `Accordion` |

Accordion uses Collapsible internally. The CSS variable for height animation differs:
- Collapsible: `--loke-collapsible-content-height`
- Accordion: `--loke-accordion-content-height`

```tsx
// Single section → Collapsible
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@loke/ui/collapsible";

<Collapsible>
  <CollapsibleTrigger>Show details</CollapsibleTrigger>
  <CollapsibleContent>
    <p>Details here</p>
  </CollapsibleContent>
</Collapsible>

// Multiple sections → Accordion
import { Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent } from "@loke/ui/accordion";

<Accordion type="single" collapsible>
  <AccordionItem value="a">
    <AccordionHeader>
      <AccordionTrigger>Section A</AccordionTrigger>
    </AccordionHeader>
    <AccordionContent>Content A</AccordionContent>
  </AccordionItem>
  <AccordionItem value="b">
    <AccordionHeader>
      <AccordionTrigger>Section B</AccordionTrigger>
    </AccordionHeader>
    <AccordionContent>Content B</AccordionContent>
  </AccordionItem>
</Accordion>
```

### Checkbox vs Switch

| Condition | Use |
|---|---|
| On/off toggle for a single setting | `Switch` |
| Multi-select list of options | `Checkbox` |
| Tri-state / indeterminate (select all) | `Checkbox` |
| `role="switch"` semantics required | `Switch` |
| `role="checkbox"` with indeterminate | `Checkbox` |

Switch has no indeterminate state. Checkbox supports `checked="indeterminate"`. Do not use Switch where indeterminate state is needed.

```tsx
// On/off toggle → Switch
import { Switch, SwitchThumb } from "@loke/ui/switch";
import { Label } from "@loke/ui/label";

<Label htmlFor="notifications">
  Enable notifications
  <Switch id="notifications">
    <SwitchThumb />
  </Switch>
</Label>

// Multi-select or indeterminate → Checkbox
import { Checkbox, CheckboxIndicator } from "@loke/ui/checkbox";

<Checkbox checked="indeterminate" onCheckedChange={handleChange}>
  <CheckboxIndicator />
</Checkbox>
```

## Common Mistakes

**1. Importing from `@loke/ui` root or Radix paths**
The barrel export does not exist. The API resembles Radix UI, so agents hallucinate `@radix-ui/react-*` paths. Neither work.
```tsx
// Wrong
import { Dialog } from "@loke/ui";
import { Dialog } from "@radix-ui/react-dialog";
// Correct
import { Dialog, DialogPortal, DialogOverlay, DialogContent, DialogTitle } from "@loke/ui/dialog";
```

**2. Hallucinating sub-components**
`DialogHeader`, `DialogFooter`, `SelectSearch` do not exist in `@loke/ui`. These are styled wrappers that may exist in `@loke/design-system` but are not primitives. Only use documented exports.

**3. Defaulting to native HTML**
`<select>`, `<dialog>`, `<input type="checkbox">` lack the accessibility behavior, keyboard navigation, and composability the primitives provide. Always use `@loke/ui` primitives.

**4. Skipping portals**
In development, rendering `DialogContent` or `PopoverContent` inline may appear to work. In production, content gets clipped by `overflow:hidden` ancestors or renders behind sticky navbars. Always include `DialogPortal` / `PopoverPortal` / `TooltipPortal`.
```tsx
// Wrong — content renders inline in DOM
<Dialog>
  <DialogTrigger>Open</DialogTrigger>
  <DialogContent>...</DialogContent>
</Dialog>

// Correct — content portals to document.body
<Dialog>
  <DialogTrigger>Open</DialogTrigger>
  <DialogPortal>
    <DialogOverlay />
    <DialogContent>...</DialogContent>
  </DialogPortal>
</Dialog>
```

**5. "Elegant" trees that skip required intermediaries**
Real `@loke/ui` trees are verbose. If the code looks concise, required components are probably missing. These are all required:
- `Dialog`: `DialogPortal` + `DialogOverlay` + `DialogTitle`
- `Select`: `SelectPortal` + `SelectContent` + `SelectViewport` + `SelectItem` + `SelectItemText`
- `Tooltip`: `TooltipProvider` + `TooltipPortal`
- `DropdownMenu`: `DropdownMenuPortal` + `DropdownMenuContent`

**6. Using Dialog for destructive confirmations**
Dialog can be dismissed with outside-click or Escape and does not enforce cancel-first focus. Use `AlertDialog` for any action that cannot be undone.

**7. Using Tooltip for interactive content**
Tooltip closes on `pointerdown` and `click`. Any interactive element inside a Tooltip (button, link, input) is unreachable. Use `Popover` instead.

**8. Using Select for async or filterable options**
Select requires all items in the tree at open time. Async data or search input requires `Popover` + `Command`.

## Start Here — Learning Path

Start with `Dialog`. It teaches every pattern used across all 19 components:
1. **Portal rendering** — `DialogPortal` teaches why portals are required
2. **Overlay semantics** — `DialogOverlay` + `DialogContent` teaches the two-layer pattern
3. **Accessible labelling** — `DialogTitle` + `DialogDescription` teaches aria-labelledby/aria-describedby
4. **Exit animations** — `forceMount` on `DialogPortal`/`DialogOverlay`/`DialogContent` teaches the `Presence` state machine
5. **`asChild` composition** — `DialogTrigger asChild` on an existing button teaches prop merging

Once Dialog is understood, every other overlay (Popover, Tooltip, DropdownMenu, AlertDialog) is the same infrastructure with different dismiss and focus rules.

## Cross-References

- [`dialog`](../dialog/SKILL.md) — modal/non-modal, focus trapping, forceMount animations
- [`alert-dialog`](../alert-dialog/SKILL.md) — forced modal, cancel-first focus, destructive patterns
- [`popover`](../popover/SKILL.md) — floating panels, modal vs non-modal, CSS custom properties
- [`tooltip`](../tooltip/SKILL.md) — hover/focus only, aria-describedby, TooltipProvider requirement
- [`dropdown-menu`](../dropdown-menu/SKILL.md) — action menus, onSelect, submenu patterns
- [`select`](../select/SKILL.md) — form value selection, SelectItemText, positioning modes
- [`command`](../command/SKILL.md) — searchable lists, Popover+Command combobox pattern
- [`accordion`](../accordion/SKILL.md) — multi-section disclosure, type prop, CSS variable animation
- [`collapsible`](../collapsible/SKILL.md) — single section, forceMount, CSS variable animation
- [`checkbox`](../checkbox/SKILL.md) — indeterminate state, form participation
- [`switch`](../switch/SKILL.md) — on/off toggle, SwitchThumb, no indeterminate
