# BottomNavigation

A mobile bottom navigation bar: `role="tablist"` with each item `role="tab"` + synced `aria-selected`; arrow keys move focus (roving tabindex), Enter/Space selects; the active item uses the primary color plus an icon, with a thin top divider.

> The layout is static by default; add the `fixed` attribute to pin it to the bottom of the screen (`position: fixed; bottom: 0` — preferably as a direct child of `body` to avoid being carried away by a scroll container).

## Basic usage

Pass a JSON array via `items` `[{ label, value, icon? }]`; `icon` is an icon name from `@oas-ui/icons` built-in icon set. When `value` is not set, the first non-disabled item is active by default.

<DemoBlock title="Basic usage">
  <oas-bottom-navigation id="bn-basic" value="home" style="width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Search","icon":"search","value":"search"},{"label":"Messages","icon":"mail","value":"mail"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

## Controlled value

The `value` attribute is controlled: setting it externally switches the active item, and component interaction also writes the attribute back and fires `oas-change`.

<DemoBlock title="Controlled switching">
  <oas-bottom-navigation id="bn-ctrl" value="home" style="width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Favorites","icon":"star","value":"favorite"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
  <oas-button-group>
    <oas-button size="small" type="primary" onclick="bnSet('home')">Home</oas-button>
    <oas-button size="small" onclick="bnSet('favorite')">Favorites</oas-button>
    <oas-button size="small" onclick="bnSet('mine')">Mine</oas-button>
  </oas-button-group>
  <span id="bn-out" style="width: 100%; color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm)"></span>
</DemoBlock>

## Disabled items

`disabled` items sync `aria-disabled`, cannot be selected by click, and are skipped by arrow-key navigation.

<DemoBlock title="Disabled items">
  <oas-bottom-navigation value="home" style="width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Discover","icon":"heart","value":"discover","disabled":true},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

## Fixed to the bottom (fixed)

Add the `fixed` attribute to pin it to the bottom of the viewport (`bottom: 0`). This demo page uses a static layout to avoid covering content; use `fixed` in real mobile scenarios.

<DemoBlock title="fixed demo (kept static here)">
  <oas-bottom-navigation fixed style="position: static; width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Search","icon":"search","value":"search"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

<script setup>
import { onMounted } from 'vue'
onMounted(() => {
  const ctrl = document.getElementById('bn-ctrl')
  const out = document.getElementById('bn-out')
  ctrl?.addEventListener('oas-change', (e) => {
    out.textContent = `oas-change: { value: "${e.detail.value}" }`
  })
  window.bnSet = (value) => ctrl?.setAttribute('value', value)
})
</script>

## Declarative child channel

Besides the `items` JSON, items can be declared with `<oas-bottom-navigation-item>` child elements (the `items` attribute **takes priority when explicitly set**; otherwise children are parsed and converge to the same rendering path). The default slot text becomes the label; attributes map to the `BottomNavItem` fields: `value` / `icon` / `disabled` / `badge`. Child additions/removals, attribute and text changes re-render automatically (MutationObserver).

<DemoBlock title="Declarative children (icon / disabled)">
  <oas-bottom-navigation id="bn-decl" value="home" style="width: 100%; max-width: 480px">
    <oas-bottom-navigation-item value="home" icon="user">Home</oas-bottom-navigation-item>
    <oas-bottom-navigation-item value="discover" icon="heart" disabled>Discover</oas-bottom-navigation-item>
    <oas-bottom-navigation-item value="mine" icon="gear">Mine</oas-bottom-navigation-item>
  </oas-bottom-navigation>
</DemoBlock>

## Corner badge (badge)

Add `badge` (number/text) to an item to render a top-right corner badge overlaid on the icon; unset items render no badge. The badge styling reuses the badge component's tokens (`--oas-badge-bg` / `--oas-badge-on-color`, default danger); the items channel and the declarative child channel behave identically.

<DemoBlock title="Badges (items channel)">
  <oas-bottom-navigation id="bn-badge" value="home" style="width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Messages","icon":"mail","value":"mail","badge":"5"},{"label":"Mine","icon":"gear","value":"mine","badge":"new"}]'></oas-bottom-navigation>
</DemoBlock>

<DemoBlock title="Badges (child channel)">
  <oas-bottom-navigation id="bn-badge-decl" value="home" style="width: 100%; max-width: 480px">
    <oas-bottom-navigation-item value="home" icon="user">Home</oas-bottom-navigation-item>
    <oas-bottom-navigation-item value="mail" icon="mail" badge="99+">Messages</oas-bottom-navigation-item>
    <oas-bottom-navigation-item value="mine" icon="gear">Mine</oas-bottom-navigation-item>
  </oas-bottom-navigation>
</DemoBlock>

## Safe area (safe-area)

The `safe-area` boolean attribute (to be combined with `fixed`): in fixed mode it adds `env(safe-area-inset-bottom)` bottom padding to avoid the home indicator of notched screens; it has no effect in non-fixed mode. This demo page stays static; use `fixed` + `safe-area` on real mobile devices.

<DemoBlock title="safe-area demo (kept static here)">
  <oas-bottom-navigation fixed safe-area style="position: static; width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Search","icon":"search","value":"search"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

## Hide on scroll (hide-on-scroll)

The `hide-on-scroll` boolean attribute (to be combined with `fixed`): the bar slides out with `translateY(100%)` when scrolling down and slides back when scrolling up; the transition only animates transform and never touches layout. A scroll delta >4px decides the direction, avoiding false triggers from tiny jitters.

> The hide is **purely visual**: no `aria-hidden` is added (tab semantics and keyboard focus stay reachable), and it is restored on scroll-back — ideal for giving screen space back while browsing long content, with the navigation still there when you scroll back up.

<DemoBlock title="Hide on scroll (fixed + hide-on-scroll)">
  <div style="width: 100%; max-width: 480px">
    <p style="color: var(--oas-color-text-secondary); font-size: var(--oas-font-size-sm); margin: 0 0 var(--oas-space-3)">Scroll this page: the bottom navigation slides out while scrolling down and slides back while scrolling up. The demo bar is fixed to the viewport bottom (centered, max-width 480px) for demonstration only.</p>
    <div style="height: 600px; display: flex; align-items: center; justify-content: center; border: 1px dashed var(--oas-color-border); border-radius: var(--oas-radius-md); color: var(--oas-color-text-tertiary); font-size: var(--oas-font-size-sm)">Long-page scroll area (try scrolling the page)</div>
  </div>
  <oas-bottom-navigation fixed hide-on-scroll style="max-width: 480px; margin: 0 auto" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Search","icon":"search","value":"search"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

## Horizontal layout (layout)

`layout="horizontal"` puts the icon on the left and the text on the right on the same row (default `stacked` puts the icon above the text); badges still overlay the icon's top-right corner; keyboard roving and ARIA semantics are unchanged. Invalid values fall back to `stacked` with a warning.

<DemoBlock title="Horizontal layout (icon left, text right)">
  <oas-bottom-navigation id="bn-layout-h" layout="horizontal" value="home" style="width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Messages","icon":"mail","value":"mail","badge":"5"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

## Icon-only compact mode (show-label)

`show-label="active"` shows text only on the selected item; unselected items show just the icon (compact mode). Unselected labels are visually hidden (CSS `display: none`) while the selected item keeps its text; every tab's accessible name is backed by an auto-written `aria-label` (the item's label text), so screen readers are unaffected by the CSS hide. Default `true` shows text on all items; invalid values fall back to `true` with a warning (deduped).

<DemoBlock title="Icon-only compact mode (text only on the selected item)">
  <oas-bottom-navigation id="bn-label-active" show-label="active" value="home" style="width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Messages","icon":"mail","value":"mail","badge":"5"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

## Selected-item lift animation (shift)

The `shift` boolean attribute lifts the selected item: its icon moves up slightly (`translateY(-2px)`) and scales up (`scale 1.08`) with the label scaling up too, while unselected items settle back; a pure CSS transition (transform only, no layout impact), disabled under `prefers-reduced-motion`.

<DemoBlock title="shift selected-item animation">
  <oas-bottom-navigation id="bn-shift" shift value="home" style="width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Search","icon":"search","value":"search"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

Click to switch the selected item and observe the icon lifting/scaling up and unselected items settling back smoothly.

## Pill shape (pill)

`pill` turns the whole bar into a **floating capsule**: the `tablist` is fully rounded, gets a full outline (replacing the top-only divider) and a soft shadow; the host insets itself on both sides and, with `fixed`, also leaves bottom spacing so it floats on all edges. Insets / shadow are exposed via the `--oas-bottom-navigation-pill-inset` / `--oas-bottom-navigation-pill-shadow` variables; dark mode picks up tokens automatically.

<DemoBlock title="Pill shape (static)">
  <oas-bottom-navigation pill value="home" style="width: 100%; max-width: 480px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Search","icon":"search","value":"search"},{"label":"Mail","icon":"mail","value":"mail","badge":"5"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

<DemoBlock title="Tunable insets / shadow (CSS variables)">
  <oas-bottom-navigation pill value="home" style="width: 100%; max-width: 480px; --oas-bottom-navigation-pill-inset: 24px; --oas-bottom-navigation-pill-shadow: 0 6px 18px rgb(0 0 0 / 0.16)" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Search","icon":"search","value":"search"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

> On real mobile add `fixed` (optionally with `safe-area`) for a bottom-floating capsule (all edges inset automatically); this demo stays static so it does not cover the content.

## Variable customization

Pure CSS variable openings (no attribute); dark mode picks up tokens automatically:

- `--oas-bottom-navigation-active-color`: active item color, defaults to the primary token
- `--oas-bottom-navigation-height`: bar height, defaults to `56px` (applies in every layout mode, not only fixed)

<DemoBlock title="Variable customization (active color + height)">
  <oas-bottom-navigation id="bn-var" value="home" style="width: 100%; max-width: 480px; --oas-bottom-navigation-active-color: var(--oas-color-success-text); --oas-bottom-navigation-height: 64px" items='[{"label":"Home","icon":"user","value":"home"},{"label":"Search","icon":"search","value":"search"},{"label":"Mine","icon":"gear","value":"mine"}]'></oas-bottom-navigation>
</DemoBlock>

## API

### oas-bottom-navigation

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `fixed` | Pin to the viewport bottom (`position: fixed; bottom: 0`) | `boolean` | — |
| `hide-on-scroll` | Hide on scroll (requires `fixed`): slides out with `translateY(100%)` while scrolling down and slides back while scrolling up (a scroll delta >4px decides the direction; the transition animates transform only). A purely visual collapse without `aria-hidden` (tab semantics and keyboard focus stay reachable), restored on scroll-back; no effect when not fixed | `boolean` | — |
| `items` | Navigation items JSON | `string` | `[]` |
| `layout` | Layout mode: `stacked` (default, icon above text) / `horizontal` (icon left, text right, same row); invalid values fall back to `stacked` with a warning (deduped) | `string` | `stacked` |
| `pill` | Pill shape: turns the whole bar into a floating capsule (`tablist` fully rounded + a full outline replacing the top-only divider + a soft shadow; the host insets itself on both sides and, in `fixed` mode, leaves bottom spacing to float on all edges). Insets/shadow are exposed via the `--oas-bottom-navigation-pill-inset` / `--oas-bottom-navigation-pill-shadow` variables; dark theme follows tokens automatically | — | — |
| `safe-area` | Adds bottom safe-area padding (`env(safe-area-inset-bottom)`) in fixed mode to avoid the home indicator of notched screens; no effect in non-fixed mode | `boolean` | — |
| `shift` | Selected-item lift animation: the selected tab's icon lifts slightly (translateY(-2px)) and scales up (scale 1.08) with the label scaling up too, unselected items settle back; pure CSS transition (transform only, no layout impact), disabled under prefers-reduced-motion | `boolean` | — |
| `show-label` | Label display mode: `true` (default, all items show text) / `active` (icon-only compact mode: only the selected item shows text, unselected items show only the icon — each tab's accessible name is backed by an auto-written `aria-label` from the item label, unaffected by the CSS hide); invalid values fall back to `true` with a warning (deduped) | `string` | `true` |
| `value` | Value of the active item; defaults to the first available item | — | — |

#### Events

| Event | Description |
| --- | --- |
| `oas-change` | The active item changed, `detail: { value }` |

#### CSS Variables

| CSS Variable | Description | Default |
| --- | --- | --- |
| `--oas-badge-bg` | — | `var(--oas-color-danger)` |
| `--oas-badge-on-color` | — | `var(--oas-color-text-on-danger)` |
| `--oas-bottom-navigation-active-color` | — | `var(--oas-color-primary)` |
| `--oas-bottom-navigation-height` | — | `56px` |
| `--oas-bottom-navigation-pill-inset` | — | `12px` |
| `--oas-bottom-navigation-pill-shadow` | — | `var(--oas-shadow-sm)` |
| `--oas-glass-blur` | — | `none` |
| `--oas-glass-refraction` | Liquid-glass edge refraction filter (active under `data-glass`, default none); for local opt-out override with an **empty** value (`--oas-glass-refraction: ;`)—not `none` (mixing `none` with button-state brightness() invalidates the whole filter declaration) | `none` |
| `--oas-glass-ring` | — | `transparent` |

### oas-bottom-navigation-item

#### Attributes

| Attribute | Description | Type | Default |
| --- | --- | --- | --- |
| `badge` | Top-right corner badge (number/text overlaid on the icon; not rendered when unset) | — | — |
| `disabled` | Disable this item (not selectable, skipped by keyboard) | — | — |
| `icon` | Leading icon (`@oas-ui/icons` registry icon name) | — | — |
| `value` | Selection value (data-carrier field of the declarative child channel) | — | — |

#### Slots

| Name | Description |
| --- | --- |
| default | Bottom-navigation item label content (default slot text) |

`BottomNavItem` fields:

| Field      | Description                                             | Type     |
| ---------- | ------------------------------------------------------- | -------- |
| `label`    | Text                                                    | `string` |
| `value`    | Value (unique identifier)                               | `string` |
| `icon`     | Icon name (a key of `@oas-ui/icons` built-in icon set)     | `string` |
| `disabled` | Disabled (not selectable, skipped by keyboard)          | `boolean`|
| `badge`    | Top-right corner badge (number/text overlaid on the icon; not rendered when unset) | `string` |

Behavior: `role="tablist"` + `role="tab"` + synced `aria-selected` / `aria-disabled`; roving tabindex keeps only the active item focusable; arrow keys (left/right or up/down) cycle focus among available items (Home/End jump to the ends), Enter/Space selects the focused item; clicking an already-active item does not re-fire; empty `items` renders an empty tablist without errors. The active item uses the primary color plus an icon (built-in inline SVG following `currentColor`), with a thin top divider; `badge` renders a top-right corner badge (overlaid on the icon, badge tokens); `fixed` + `safe-area` adds bottom safe-area padding.
