---
name: interactive-components
description: >
  Use interactive design system components and the asChild/Slot pattern.
  Button (variants: default/destructive/ghost/link/outline/secondary, sizes:
  default/sm/lg/icon/icon-sm/icon-xs/icon-lg, width, justify, asChild),
  Input (auto-icon detection by type: search/email/password/time/date,
  onClear callback, custom icon prop), Textarea, Accordion/AccordionItem/
  AccordionTrigger/AccordionContent, Tabs/TabsList/TabsTrigger/TabsContent,
  Collapsible/CollapsibleTrigger/CollapsibleContent, Label, Slot/Slottable/
  createSlot for asChild composition. Activate when adding buttons, inputs,
  accordions, tabs, or using the asChild pattern.
type: core
library: '@loke/design-system'
library_version: '2.0.0-rc.6'
requires:
  - getting-started
sources:
  - 'LOKE/merchant-frontends:packages/design-system/src/components/button'
  - 'LOKE/merchant-frontends:packages/design-system/src/components/input'
  - 'LOKE/merchant-frontends:packages/design-system/src/components/textarea'
  - 'LOKE/merchant-frontends:packages/design-system/src/components/accordion'
  - 'LOKE/merchant-frontends:packages/design-system/src/components/tabs'
  - 'LOKE/merchant-frontends:packages/design-system/src/components/collapsible'
  - 'LOKE/merchant-frontends:packages/design-system/src/components/label'
  - 'LOKE/merchant-frontends:packages/design-system/src/components/slot'
---

# Interactive Components

This skill builds on **getting-started**. Read it first for setup and imports.

## Setup

```tsx
import { Button } from "@loke/design-system/button";
import { Input } from "@loke/design-system/input";
import { Slot } from "@loke/design-system/slot";

// Button with variant
<Button variant="destructive" size="sm">Delete</Button>

// Input with auto-icon by type
<Input type="search" placeholder="Search..." onClear={() => setValue("")} />

// asChild pattern — render as a link instead of a button
<Button asChild>
  <a href="/docs">Documentation</a>
</Button>
```

## Core Patterns

### Button variants and sizes

Button accepts `variant`, `size`, `width`, `justify`, and `asChild` props.

**Variants** (6 total):

| Variant | Usage |
|---|---|
| `default` | Primary actions (submit, save) |
| `destructive` | Delete, remove, dangerous actions |
| `ghost` | Subtle actions, toolbar buttons |
| `link` | Text-only link styling with underline on hover |
| `outline` | Secondary actions with visible border |
| `secondary` | Less prominent actions |

**Sizes** (7 total):

| Size | Output |
|---|---|
| `default` | `h-10 px-4 py-2` |
| `sm` | `h-9 rounded-md px-3` |
| `lg` | `h-11 rounded-md px-8` |
| `icon` | `size-8` (square, default icon button) |
| `icon-sm` | `size-7` |
| `icon-xs` | `size-6` (smallest icon button, svg shrinks to size-3) |
| `icon-lg` | `size-9` |

**Width and justify:**

```tsx
<Button width="full" justify="between">
  Select option <ChevronDownIcon />
</Button>
```

**asChild for links** -- prevents nested interactive elements (button > a):

```tsx
import { Button } from "@loke/design-system/button";

<Button asChild variant="outline">
  <a href="/docs">Go to docs</a>
</Button>
```

**Icon buttons:**

```tsx
import { Button } from "@loke/design-system/button";
import { Trash2 } from "@loke/icons";

<Button variant="ghost" size="icon-sm">
  <Trash2 />
</Button>
```

### Input with auto-icon and clear

`Input` auto-assigns leading icons based on `type`. Pass `icon` to override. Pass `onClear` for a clear button.

**Auto-icon mapping:**

| `type` | Icon |
|---|---|
| `search` | `Search` |
| `email` | `Mail` |
| `password` | `Lock` |
| `time` | `Clock` |
| `date` / `datetime-local` | `Calendar` |

```tsx
import { Input } from "@loke/design-system/input";

// Search with auto icon + clear button
<Input
  type="search"
  placeholder="Search products..."
  value={query}
  onChange={(e) => setQuery(e.target.value)}
  onClear={() => setQuery("")}
/>

// Email with auto icon
<Input type="email" placeholder="you@example.com" />

// Custom icon override
import { DollarSign } from "@loke/icons";

<Input icon={DollarSign} type="number" placeholder="0.00" />
```

Props: `InputProps = InputHTMLAttributes<HTMLInputElement> & { icon?: LokeIcon; onClear?: () => void }`

### Textarea

Standard multi-line input. No special props beyond `TextareaHTMLAttributes<HTMLTextAreaElement>`.

```tsx
import { Textarea } from "@loke/design-system/textarea";

<Textarea placeholder="Enter a description..." rows={4} />
```

### Accordion

Collapsible content panels. Supports `type="single"` (one open at a time) or `type="multiple"` (independent).

```tsx
import {
  Accordion,
  AccordionItem,
  AccordionTrigger,
  AccordionContent,
} from "@loke/design-system/accordion";

<Accordion type="single" collapsible>
  <AccordionItem value="item-1">
    <AccordionTrigger>What is your refund policy?</AccordionTrigger>
    <AccordionContent>
      We offer a 30-day money-back guarantee on all plans.
    </AccordionContent>
  </AccordionItem>
  <AccordionItem value="item-2">
    <AccordionTrigger>How do I cancel?</AccordionTrigger>
    <AccordionContent>
      Go to Settings and select Cancel Subscription.
    </AccordionContent>
  </AccordionItem>
</Accordion>
```

- Each `AccordionItem` requires a unique `value` string.
- `AccordionTrigger` renders chevron icons automatically (down when closed, up when open).
- `collapsible` prop on `Accordion` allows all items to be closed when `type="single"`.

### Tabs

Organize content into selectable tab panels. Supports `orientation="horizontal"` (default) and `orientation="vertical"`.

```tsx
import {
  Tabs,
  TabsList,
  TabsTrigger,
  TabsContent,
} from "@loke/design-system/tabs";

<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="analytics">Analytics</TabsTrigger>
    <TabsTrigger value="settings">Settings</TabsTrigger>
  </TabsList>
  <TabsContent value="overview">Overview content here.</TabsContent>
  <TabsContent value="analytics">Analytics content here.</TabsContent>
  <TabsContent value="settings">Settings content here.</TabsContent>
</Tabs>
```

- `TabsTrigger` `value` must match the corresponding `TabsContent` `value`.
- Active tab has an animated underline indicator.
- Keyboard navigation (arrow keys) works out of the box.

### Collapsible

Simple show/hide toggle for a single section. Lighter than Accordion when you only need one collapsible region.

```tsx
import {
  Collapsible,
  CollapsibleTrigger,
  CollapsibleContent,
} from "@loke/design-system/collapsible";
import { Button } from "@loke/design-system/button";

<Collapsible>
  <CollapsibleTrigger asChild>
    <Button variant="ghost" size="sm">Toggle details</Button>
  </CollapsibleTrigger>
  <CollapsibleContent>
    <p>Additional details shown when expanded.</p>
  </CollapsibleContent>
</Collapsible>
```

### The asChild / Slot pattern

When `asChild={true}`, a component renders its child element instead of its default DOM element, merging all props (className, event handlers, refs, aria attributes) onto the child.

**How it works internally:**

```tsx
import { createSlot } from "@loke/design-system/slot";

const ButtonSlot = createSlot("Button");

function Button({ asChild, children, className, ...props }) {
  const Comp = asChild ? ButtonSlot : "button";
  return <Comp className={className} {...props}>{children}</Comp>;
}
```

**Key exports from `@loke/design-system/slot`:**

| Export | Purpose |
|---|---|
| `Slot` | Merges props into a single child element |
| `Slottable` | Marks content as replaceable within a slotted layout |
| `createSlot(ownerName)` | Creates a namespaced Slot (e.g., `Button.Slot`) |
| `createSlottable(ownerName)` | Creates a namespaced Slottable marker |

**Components that support `asChild`:** Button, CollapsibleTrigger, and sidebar-related compositions.

**Merge rules:**
- Event handlers compose: child handler runs first, then slot handler.
- `className` values concatenate.
- `style` objects merge (child overrides slot).
- `Slot` expects exactly one valid child element.

```tsx
import { Slot } from "@loke/design-system/slot";

// Generic polymorphic wrapper
function Card({ asChild, className, ...props }) {
  const Comp = asChild ? Slot : "div";
  return <Comp className={cn("rounded-lg border p-4", className)} {...props} />;
}

// Renders as <section> with merged className
<Card asChild>
  <section className="bg-muted">Custom card</section>
</Card>
```

## Common Mistakes

### 1. CRITICAL: Missing asChild when wrapping non-button elements

```tsx
// WRONG -- produces nested interactive elements (button > a), invalid HTML
<Button><a href="/docs">Docs</a></Button>

// CORRECT
<Button asChild><a href="/docs">Docs</a></Button>
```

Without `asChild`, Button renders a `<button>` wrapping an `<a>`, which is invalid HTML and breaks accessibility.

### 2. CRITICAL: Hallucinating props from other libraries

```tsx
// WRONG -- none of these props exist on Button
<Button isLoading leftIcon={<Spinner />} colorScheme="blue">Save</Button>

// CORRECT -- handle loading state yourself
<Button disabled={isPending}>
  {isPending && <Spinner className="mr-2" />}
  Save
</Button>
```

Agents frequently add `isLoading`, `colorScheme`, `leftIcon`, `rightIcon` from shadcn/Chakra/MUI. The `@loke/design-system` Button only accepts `variant`, `size`, `width`, `justify`, `asChild`, and standard HTML button attributes.

### 3. HIGH: Hallucinating components that don't exist

```tsx
// WRONG -- none of these exist in @loke/design-system
import { Combobox } from "@loke/design-system/combobox";
import { Drawer } from "@loke/design-system/drawer";
import { NavigationMenu } from "@loke/design-system/navigation-menu";
import { ScrollArea } from "@loke/design-system/scroll-area";
import { HoverCard } from "@loke/design-system/hover-card";
import { FormInput } from "@loke/design-system/form-input";
```

Always verify a component exists by checking `package.json` exports before importing. Combobox is built by composing Popover + Command (see overlay-composition skill).

### 4. HIGH: Wrong Button variant for destructive actions

```tsx
// WRONG -- default variant for a delete action
<Button onClick={handleDelete}>Delete account</Button>

// CORRECT
<Button variant="destructive" onClick={handleDelete}>Delete account</Button>

// Confirmation dialogs: destructive action + outline cancel
<div className="flex gap-2 justify-end">
  <Button variant="outline" onClick={onCancel}>Cancel</Button>
  <Button variant="destructive" onClick={onConfirm}>Delete</Button>
</div>
```

### 5. HIGH: Building custom search input instead of using Input type="search"

```tsx
// WRONG -- manually recreating what Input already provides
function SearchInput({ value, onChange, onClear }) {
  return (
    <div className="relative">
      <Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4" />
      <input className="pl-8 ..." value={value} onChange={onChange} />
      {value && <button onClick={onClear}><X /></button>}
    </div>
  );
}

// CORRECT -- Input handles icon + clear button automatically
import { Input } from "@loke/design-system/input";

<Input
  type="search"
  value={value}
  onChange={(e) => setValue(e.target.value)}
  onClear={() => setValue("")}
/>
```

`Input` with `type="search"` auto-renders the search icon and, when `onClear` is provided, adds a clear button. No need to build this from scratch.

## See also

- **forms/SKILL.md** -- form controls wire differently per component (Label, FormControl, validation)
- **overlay-composition/SKILL.md** -- Combobox = Popover + Command; Dialog, Sheet, Popover patterns
- **display-components/SKILL.md** -- presentational components (Badge, Card, Avatar, Separator, etc.)
