# useLayer

## Usage

Core positioning hook for rendering overlay content. Use it as the foundation for custom popovers, hover cards, tooltips, and fixed-position layers when the higher-level components are not enough.

```ts
import { useLayer } from '@astryx-vue/core'
```

`context` mode follows an anchor element; `fixed` mode places the layer at explicit `x` / `y` viewport coordinates. The hook owns the open state, the measured coordinates, click-outside dismissal, and the layer ID. It renders nothing: you decide where the layer lives and what it looks like.

```vue
<script setup lang="ts">
import { useLayer, useLayerTeleportTarget } from '@astryx-vue/core'

const layer = useLayer({ mode: 'context', placement: 'below', lightDismiss: true })
// Inside an open Dialog the layer mounts into it; everywhere else, into the body.
const teleportTarget = useLayerTeleportTarget()
</script>

<template>
  <button :ref="layer.anchorRef" type="button" @click="layer.show()">
    Show layer
  </button>
  <Teleport :to="teleportTarget">
    <div
      v-if="layer.isMounted"
      v-show="layer.isOpen"
      :id="layer.id"
      :ref="layer.contentRef"
      :style="layer.contentStyle"
    >
      Anchored layer
    </div>
  </Teleport>
</template>
```

`contentStyle` carries the measured `top` / `left` (plus `position: fixed` in fixed mode), so the surface only needs its own background, radius, and shadow. Positions are measured in the viewport, so render the layer into a `Teleport` — that also keeps it out of ancestor clipping and stacking.

### Dismissal: one press closes the top layer

Every layer shares one dismissal stack. Escape, and a press outside a layer, close only the top-most layer: with a menu open inside a Dialog, the first Escape closes the menu and leaves the Dialog up, and the second closes the Dialog. A press outside the menu but inside the Dialog closes the menu only.

A press on the layer's own trigger does not count as an outside press — the trigger owns that toggle, so the layer cannot be dismissed and reopened in one click. Escape during an IME composition cancels the composition and dismisses nothing.

A layer that drills into its own sub-levels can handle Escape itself through `useDismiss`'s `onEscape`; an outside press still closes the whole layer.

### Where the layer lands

Layers render into the body by default. An open Dialog is a native modal, so a layer outside it is painted over by the backdrop and cannot be clicked; the Dialog therefore provides its own mount point and layers prefer it. `useLayerTeleportTarget()` returns that target (or `'body'`), ready for `Teleport`.

### Text styles

A teleported layer still inherits the typography of where it was written. Put `astryx-layer` on the layer root and font size, weight, line height, alignment, letter spacing, and wrapping fall back to the body baseline; the container-padding variables are zeroed there too. Our `Popover`, `Tooltip`, `HoverCard`, `DropdownMenu`, `ContextMenu`, `TabMenu`, and `Selector` surfaces already carry these resets.

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Use context mode for anchor-positioned overlays relative to a trigger element, and fixed mode for manually positioned overlays at specific coordinates. |
| Do | Build on higher-level components like `Popover`, `HoverCard`, and `Tooltip` for common overlay patterns. |
| Do | Render the layer through `Teleport` so it escapes ancestor clipping and stacking, and keep the theme variables it needs available where it lands. |
| Don't | Implement ARIA patterns directly in a layer unless you also own the full accessibility behavior. |

## Examples

Low-level anchored overlay rendered with `useLayer` and a custom surface.

### useLayer — Anchored Layer

Low-level anchored overlay rendered with useLayer and a custom surface.

```vue
<script setup lang="ts">
import { useLayer } from '@astryx-vue/core'

const layer = useLayer({ mode: 'context', placement: 'below', alignment: 'center', gap: 8, lightDismiss: true })
const { isOpen, isMounted, contentStyle, show, hide } = layer

function toggle(): void {
  if (isOpen.value)
    hide()
  else
    show()
}
</script>

<template>
  <div class="hook-layer">
    <button :ref="layer.anchorRef" type="button" class="hook-layer__trigger" @click="toggle">
      {{ isOpen ? 'Hide layer' : 'Show layer' }}
    </button>

    <Teleport to="body">
      <div
        v-if="isMounted"
        v-show="isOpen"
        :id="layer.id"
        :ref="layer.contentRef"
        class="hook-layer__surface"
        :style="contentStyle"
      >
        <p class="hook-layer__title">
          Anchored layer
        </p>
        <p class="hook-layer__body">
          Positioned next to its anchor with useLayer. Clicking outside closes it.
        </p>
        <button type="button" class="hook-layer__action" @click="hide">
          Dismiss
        </button>
      </div>
    </Teleport>
  </div>
</template>

<style scoped>
.hook-layer {
  display: flex;
  justify-content: center;
}

.hook-layer__trigger {
  padding: 8px 16px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-control, 8px);
  background: var(--astryx-color-background-surface, #fff);
  color: var(--astryx-color-text-primary, #171717);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}

.hook-layer__surface {
  position: fixed;
  z-index: 1000;
  box-sizing: border-box;
  width: 16rem;
  padding: 12px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-container, 12px);
  background: var(--astryx-color-background-surface, #fff);
  box-shadow: 0 4px 8px oklch(0 0 0 / 10%);
  color: var(--astryx-color-text-primary, #171717);
  font-size: 0.875rem;
}

.hook-layer__title {
  margin: 0 0 4px;
  font-weight: 600;
}

.hook-layer__body {
  margin: 0 0 10px;
  color: var(--astryx-color-text-secondary, #525252);
  line-height: 1.5;
}

.hook-layer__action {
  padding: 6px 10px;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-control, 8px);
  background: transparent;
  color: inherit;
  font: inherit;
  font-size: 0.75rem;
  cursor: pointer;
}
</style>
```

## Parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | `'context' \| 'fixed'` | required | Positioning strategy: `context` follows an anchor element, `fixed` uses explicit `x` / `y` coordinates. |
| `anchor` | `Ref` | — | External anchor element for context mode. Leave it out and bind the returned `anchorRef` to the trigger instead. |
| `placement` | `'above' \| 'below' \| 'start' \| 'end' \| MaybeRefOrGetter` | `'below'` | Side of the anchor the layer is placed on. Logical: `start` / `end` mirror under RTL. Flips when there is no room. |
| `alignment` | `'start' \| 'center' \| 'end' \| MaybeRefOrGetter` | `'center'` | Alignment along the placement axis. |
| `gap` | `number` | `4` | Clearance between the anchor and the layer, in pixels. |
| `x` | `number \| MaybeRefOrGetter` | `0` | Horizontal viewport coordinate for fixed mode. |
| `y` | `number \| MaybeRefOrGetter` | `0` | Vertical viewport coordinate for fixed mode. |
| `isDefaultOpen` | `boolean` | `false` | Whether the layer is open on the first render. |
| `lightDismiss` | `boolean \| MaybeRefOrGetter` | `false` | Whether clicking outside dismisses the layer. |
| `lazyMount` | `boolean \| MaybeRefOrGetter` | `false` | Whether to wait for the first `show()` before rendering the content. `isMounted` reports the answer. |
| `onShow` | `() => void` | — | Called when `show()` opens the layer. Opening through `isDefaultOpen` is not a show, so it does not fire. |
| `onHide` | `() => void` | — | Called when `hide()` closes the layer. |

## Returns

| Field | Type | Description |
| --- | --- | --- |
| `isOpen` | `ShallowRef<boolean>` | Whether the layer is currently open. |
| `anchorRef` | `Ref` | Anchor element for context mode. Bind it with `:ref`; unset in fixed mode. |
| `contentRef` | `Ref` | Layer container. Measured for positioning and used for outside clicks. |
| `contentStyle` | `ComputedRef` | Inline coordinates: measured `top` / `left` in context mode, `position: fixed` plus `x` / `y` in fixed mode. |
| `resolvedPlacement` | `ComputedRef<'above' \| 'below' \| 'start' \| 'end'>` | Placement actually in effect after flipping for available space. Equal to the requested placement in fixed mode. |
| `isMounted` | `ComputedRef<boolean>` | Whether the content should render. Stays false until the first `show()` while `lazyMount` is on. |
| `id` | `string` | Unique ID for `aria-controls` or `aria-describedby`. |
| `anchorId` | `string` | CSS identifier for the anchor element, for advanced positioning cases. |
| `show` | `() => void` | Imperatively show the layer. |
| `hide` | `() => void` | Imperatively hide the layer. |
