@loke/ui
Version:
293 lines (239 loc) • 11.8 kB
Markdown
---
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 /ui primitive. Dialog vs AlertDialog,
Popover vs Tooltip vs DropdownMenu, Select vs Popover+Command, Accordion vs Collapsible,
Checkbox vs Switch. Import path rules: always /ui/[component], never /ui
root, never -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 `/ui` root or Radix paths**
The barrel export does not exist. The API resembles Radix UI, so agents hallucinate `-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 `/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 `/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 `/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