UNPKG

@loke/ui

Version:
293 lines (239 loc) 11.8 kB
--- 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 HereLearning 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