---
outline: deep
---

# Sticky Bar

A bar docked to the viewport edge, painted in the document's **top layer**. Pass `for="<id>"` to track an element; the bar reveals when that element scrolls out of view (e.g. an Add to cart button on a mobile product page). Omit `for` for a permanently visible bar.

Common use cases: mobile product Add to cart, sticky save action on long forms, post-form newsletter signup, cookie banners, environment indicators, promo announcements.

**`<l-sticky-bar>`** — Custom Element · Shadow DOM

## Examples

### Mobile product page

The canonical use: an Add to cart CTA stays reachable while the customer scrolls product details. Each iframe below is its own document — the sticky bar paints in its top layer and `IntersectionObserver` resolves against the iframe's viewport, so production behavior is faithfully simulated.

Both demos start with the bar **revealed** (the Add to cart button sits below the fold). Scroll inside a phone to bring the button into view — the bar hides. Keep scrolling past the button — the bar reveals again.

<div class="phone-deck">
  <div class="phone">
    <span class="label">placement="bottom"</span>
    <iframe src="/previews/sticky-bar-mobile-bottom.html" title="Sticky bar — bottom placement"></iframe>
  </div>
  <div class="phone">
    <span class="label">placement="top"</span>
    <iframe src="/previews/sticky-bar-mobile-top.html" title="Sticky bar — top placement"></iframe>
  </div>
</div>

> The top phone uses `style="--offset: var(--header-height)"` to dock under the in-page sticky header — `--header-height` is defined once at `:root` and shared between the header's `height` and the bar's offset, so they stay in sync.

```html
<button
  id="add-to-cart"
  class="l-button"
  data-variant="primary"
>
  Add to cart — €42
</button>

<l-sticky-bar for="add-to-cart">
  <div
    class="flex items-center justify-between gap-3 border-t border-[var(--l-color-divider)] bg-white px-4 py-3 dark:bg-zinc-900"
  >
    <span class="text-sm font-semibold text-primary">Magic Mouse — €42</span>
    <button
      class="l-button"
      data-variant="primary"
      data-size="sm"
    >
      Add to cart
    </button>
  </div>
</l-sticky-bar>
```

## Accessibility

The element is a positioning shell — it adds no role of its own. Slotted content keeps its native semantics: a `<button>` stays a button, a `<form>` stays a form, links remain in the focus order.

- **Motion** — Respects `prefers-reduced-motion` — the slide animation collapses to instant
- **Focus order** — Slotted content stays in the natural focus order. Do not focus-trap inside the bar — it is not a dialog
- **Contrast** — The bar inherits text and background from slotted content — apply your own contrast tokens

### Rules

- Keep the bar action self-explanatory (e.g. duplicate the in-page button label, do not introduce a new verb)
- Use `--offset` to clear a sticky header when `placement="top"` to avoid overlap
- Do not nest live regions or modal-like behavior inside the bar

## API reference

### Importing

```js
import 'luxen-ui/sticky-bar';
```

### Attributes & Properties

- **for**: `string` — HTML id of the element to track. When that element leaves the viewport, the bar reveals. Omit for a permanently visible bar.
- **root**: `string` — HTML id of the scrolling ancestor used as the IntersectionObserver root. Omit to use the viewport. Useful for nested scroll containers (CMS preview panes, modals).
- **placement**: `'bottom' | 'top'` (default: `'bottom'`) — Edge to dock against.

### Events

- **show** (cancelable) — Fired before the bar reveals. Cancelable.
- **after-show** — Fired after the reveal animation completes. Not cancelable.
- **hide** (cancelable) — Fired before the bar hides. Cancelable.
- **after-hide** — Fired after the hide animation completes. Not cancelable.

### Slots

- **(default)** — Bar content. Owns its own background, padding, and typography.

### CSS custom properties

- `--show-duration` (default: `200ms`) — Reveal animation duration.
- `--hide-duration` (default: `200ms`) — Dismiss animation duration.
- `--offset` (default: `0px`) — Distance from the active edge. Use to clear a sticky header when `placement="top"`.

> **Top layer.** The bar uses `popover="manual"` internally, so it paints in the document's top layer — `z-index` is not needed and would be ignored. Target `l-sticky-bar:popover-open` to style the revealed state.
