# Panel Switch

> Switches what a box shows for a new tab, filter, or item. The box holds still and glides to its new height.

- Name: `panel-switch`
- Status: beta
- Categories: motion, layout
- File: the install writes `panel-switch.tsx` to your `ui` alias, `@/components/ui` by default.
- Gallery: https://arbokui.com/components/panel-switch
- Registry item: https://arbokui.com/r/panel-switch.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/panel-switch
   ```

## Use it for

- A box that stays on screen while its contents change: a tab's panel, a filtered list, or the item picked in a list.
- Give each set of contents its own value. A new value switches the box. New children under the same value update in place.
- Keep buttons that act on what the box shows, like Approve or Next, outside it, so the one you press never slides away.
- Use glideFrom for a box inside that grows on its own, like a list that shows more rows. It glides on the same curve.

## Avoid it for

- A new page, a popup that opens, or a box that just appeared. Those keep their own motion.
- New cards arriving in a grid. Let each one fade up where it lands.
- Words on a button that change. Button already morphs its own words.

## Props

- `value`: `string`. What the box shows. A new value switches the contents.
- `order`: `number`. Default `0`. Where the value sits in its list. A later one slides in from the right and an earlier one from the left, mirrored in right-to-left pages.
- `effect`: `"slide" | "fade" | "instant"`. Default `"slide"`. slide moves the new contents in from the side they come from. fade changes them in place, like a question that turns into its answer. Both glide the box to its new height. instant shows the change at once, with no motion.
- `children`: `ReactNode`. What the box shows for this value.
- `glideFrom`: `(element: HTMLElement, from: number) => void`. Helper. Glides a box that just grew or shrank from the height it had, on the switch's curve. Read its height before the change, and call it after.

## Accessibility

- The leaving contents are hidden from screen readers and cannot take focus or a click.
- Focus inside the leaving contents goes to the tab that picked the new ones, or else into the new ones. With nothing there to take it, the box holds it, out of the Tab order. It is never dropped to the page.
- Reduced motion keeps the fade, and drops the slide and the height glide.
- Tell screen readers what changed, like a count of results, in a live region beside it.

## Examples

### Switch what a box shows by value

```tsx
<PanelSwitch value={tab}>
  <p>{tab}</p>
</PanelSwitch>
```

### Switch a panel's contents when a tab is picked

```tsx
const [tab, setTab] = useState("overview");

<>
  <Tabs value={tab} onValueChange={setTab}>
    <TabsList aria-label="Sections">
      <TabsTab value="overview">Overview</TabsTab>
      <TabsTab value="activity">Activity</TabsTab>
    </TabsList>
  </Tabs>
  <PanelSwitch value={tab} order={tab === "overview" ? 0 : 1}>
    {tab === "overview" ? <p>Three items are open.</p> : <p>Two items changed today.</p>}
  </PanelSwitch>
</>
```

### Change a question into its answer in place

```tsx
<PanelSwitch value={picked ? "done" : "pick"} effect="fade">
  {picked ? <p>Owner: {picked}</p> : <button type="button" onClick={() => setPicked("Tom Hale")}>Pick an owner</button>}
</PanelSwitch>
```

### Show a change at once when a link lands on hidden contents

```tsx
<PanelSwitch value={item} order={index} effect={fromLink ? "instant" : "slide"}>
  <p>{item}</p>
</PanelSwitch>
```

### Glide a list inside the box as it shows more rows

```tsx
const rows = ["First", "Second", "Third", "Fourth", "Fifth"];
const list = useRef<HTMLUListElement>(null);
const before = useRef(0);
const [open, setOpen] = useState(false);

useLayoutEffect(() => {
  if (open && list.current) glideFrom(list.current, before.current);
}, [open]);

<>
  <ul ref={list}>
    {(open ? rows : rows.slice(0, 3)).map((row) => (
      <li key={row}>{row}</li>
    ))}
  </ul>
  <button
    type="button"
    onClick={() => {
      before.current = list.current?.offsetHeight ?? 0;
      setOpen(true);
    }}
  >
    Show more
  </button>
</>
```
