# ViewToggle

> Switches how a collection is shown (table, grid, board, list or calendar), with one view always pressed and optional persistence in localStorage.

Source: https://docs.nasaqui.com/components/view-toggle

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/view-toggle.json
```

A segmented control for picking how a collection is shown: a table, a grid of cards, a board, a list or a calendar. Exactly one view is pressed at a time. With `storageKey` the choice is remembered, so the page opens in the view the visitor left it in.

## When to use

- In a list page's toolbar, when the same records can be read as a table or as cards (or a board, a calendar).

## When not to use

- Switching between different content: use `Tabs`.
- Toggling formatting or filters: use [`ToggleGroup`](https://docs.nasaqui.com/components/toggle-group) directly.

## Import

```tsx
import { ViewToggle } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { ViewToggle, type ViewMode } from "@fadymondy/nasaq/web";

const [view, setView] = useState<ViewMode>("table");

<ViewToggle views={["table", "grid"]} value={view} onValueChange={setView} storageKey="customers:view" />;
```

## Anatomy

```
div [data-slot=view-toggle]
└─ ToggleGroup (segmented, aria-label "View")
   └─ Toggle [data-view=table|grid|board|list|calendar] (icon; label when `showLabels`, tooltip otherwise)
```

## API

### `ViewToggle`

| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `views` | `ViewMode[]` | `["table", "grid"]` | The views offered, in order. |
| `value` | `ViewMode` | — | Controlled view. |
| `defaultValue` | `ViewMode` | first of `views` | Uncontrolled start. |
| `onValueChange` | `(view: ViewMode) => void` | — | Also called once after mount when a stored view is restored. |
| `storageKey` | `string` | — | Remembers the choice in `localStorage`. Read after mount, so SSR stays stable. |
| `showLabels` | `boolean` | `false` | Text beside each icon. Icon-only items get an `aria-label` and a tooltip. |
| `labels` | `Partial<ViewToggleLabels>` | — | Override the group name and view names. |

Plus any `<div>` prop. `ViewMode` is `"table" | "grid" | "board" | "list" | "calendar"`.

## Examples

### With labels

```tsx
<ViewToggle views={["table", "grid", "list"]} showLabels />
```

### Board and calendar for work items

```tsx
<ViewToggle views={["board", "list", "calendar"]} defaultValue="board" storageKey="issues:view" onValueChange={setView} />
```

## Accessibility

- A Base UI toggle group: arrow keys move between views, Space and Enter press.
- The group is named "View" / "طريقة العرض"; icon-only items carry an `aria-label` with the view's name.
- Pressing the pressed view does nothing, so the group never ends up with no view.

## RTL & i18n

View names ship in English and Arabic. Arrow keys follow the reading direction.

## Styling & tokens

- Inherits `ToggleGroup`'s segmented look: `bg-secondary` track, `bg-card` pressed item, `text-label`.
- Target a view with `[data-slot=view-toggle] [data-view=grid]`.

## Do / Don't

- Do put it at the inline end of the list toolbar, next to filters.
- Do use `storageKey` with a per-page key.
- Don't offer views the page can't render well; two or three is typical.

## Related

`toggle-group`, `data-table`, `kanban-board`, `page-header`.

## Lab

https://docs.nasaqui.com/?path=/docs/components-actions-view-toggle--docs

## Code

### React

```tsx
import { ViewToggle, type ViewMode } from "@fadymondy/nasaq/web";

const [view, setView] = useState<ViewMode>("table");

<ViewToggle views={["table", "grid"]} value={view} onValueChange={setView} storageKey="customers:view" />;
```

### shadcn

```tsx
import { ViewToggle, type ViewMode } from "@/components/ui/view-toggle";

const [view, setView] = useState<ViewMode>("table");

<ViewToggle views={["table", "grid"]} value={view} onValueChange={setView} storageKey="customers:view" />;
```

### Vue

```vue
<script setup lang="ts">
import { NqViewToggle, type ViewMode } from "@fadymondy/nasaq/vue";
import { ref } from "vue";

const view = ref<ViewMode>("table");
</script>

<template>
  <NqViewToggle v-model="view" :views="['table', 'grid']" storage-key="customers:view" />
</template>
```

### Blade

```blade
<x-nq::view-toggle :views="['table', 'grid']" storage-key="customers:view" />
```

### HTML + Alpine

```html
<div data-slot="view-toggle" x-data="nqViewToggle('table', JSON.parse('[\u0022table\u0022,\u0022grid\u0022]'), 'customers:view')" x-modelable="value" class="inline-flex">
    <div role="group" aria-label="View" data-slot="toggle-group" data-variant="segmented" data-orientation="horizontal" x-bind="group" class="flex w-fit max-w-full gap-0.5 rounded-control bg-secondary p-0.5">
                                                <div data-slot="tooltip" x-data="nqTooltip(600, false)" x-modelable="open" x-id="['nq-tooltip']" class="contents">
    <span data-slot="tooltip-trigger" class="contents"><button type="button" data-slot="toggle" data-view="table" aria-label="Table" aria-pressed="true"  data-pressed  x-bind="item('table')" class="inline-flex h-7 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap px-3 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:pointer-events-none data-disabled:opacity-50 rounded-[calc(var(--radius-control)-2px)] data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs">
                        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/>
</svg>                    </button></span>
    <template x-teleport="body">
        <div data-slot="tooltip-content" x-bind="popup" x-nq-presence="open" x-anchor.top.offset.6="triggerEl"
            class="z-50 max-w-64 rounded-control bg-foreground px-2 py-1 text-caption text-background transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">Table</div>
    </template>
</div>
                                                            <div data-slot="tooltip" x-data="nqTooltip(600, false)" x-modelable="open" x-id="['nq-tooltip']" class="contents">
    <span data-slot="tooltip-trigger" class="contents"><button type="button" data-slot="toggle" data-view="grid" aria-label="Grid" aria-pressed="false"  x-bind="item('grid')" class="inline-flex h-7 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap px-3 text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground [&amp;_svg]:size-4 [&amp;_svg]:shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:pointer-events-none data-disabled:opacity-50 rounded-[calc(var(--radius-control)-2px)] data-pressed:bg-card data-pressed:text-foreground data-pressed:shadow-xs">
                        <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <rect width="7" height="7" x="3" y="3" rx="1"/>
  <rect width="7" height="7" x="14" y="3" rx="1"/>
  <rect width="7" height="7" x="14" y="14" rx="1"/>
  <rect width="7" height="7" x="3" y="14" rx="1"/>
</svg>                    </button></span>
    <template x-teleport="body">
        <div data-slot="tooltip-content" x-bind="popup" x-nq-presence="open" x-anchor.top.offset.6="triggerEl"
            class="z-50 max-w-64 rounded-control bg-foreground px-2 py-1 text-caption text-background transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">Grid</div>
    </template>
</div>
                        </div>
</div>
```
