Popover
Non-modal overlay primitive with shared open/close lifecycle. Click activation toggles from its anchor by default; trigger=manual retains anchoring, expanded state, Escape and light dismissal while the caller controls open, including when switching between anchor controls.
Trigger binding
The for property connects Popover to the Looma button that toggles it.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-popover/ui-popover.html"
>
<ui-button
id="popover-trigger"
variant="ghost"
>Office briefing</ui-button>
<ui-popover
id="example-popover"
for="popover-trigger"
>Sam has booked the quiet room. Frodo is bringing the maps.</ui-popover><script setup>
import { UiButton, UiPopover } from "@threadlabs/looma";
</script>
<template>
<UiButton
id="popover-trigger"
variant="ghost"
>Office briefing</UiButton>
<UiPopover
id="example-popover"
for="popover-trigger"
>Sam has booked the quiet room. Frodo is bringing the maps.</UiPopover>
</template>import { UiButton, UiPopover } from "@threadlabs/looma";
export default function Example() {
return <>
<UiButton
id="popover-trigger"
variant="ghost"
>Office briefing</UiButton>
<UiPopover
id="example-popover"
htmlFor="popover-trigger"
>Sam has booked the quiet room. Frodo is bringing the maps.</UiPopover>
</>;
}/** @jsxImportSource preact */
import { UiButton, UiPopover } from "@threadlabs/looma";
export default function Example() {
return <>
<UiButton
id="popover-trigger"
variant="ghost"
>Office briefing</UiButton>
<UiPopover
id="example-popover"
htmlFor="popover-trigger"
>Sam has booked the quiet room. Frodo is bringing the maps.</UiPopover>
</>;
}<script>
import { UiButton, UiPopover } from "@threadlabs/looma";
</script>
<UiButton
id="popover-trigger"
variant="ghost"
>Office briefing</UiButton>
<UiPopover
id="example-popover"
for="popover-trigger"
>Sam has booked the quiet room. Frodo is bringing the maps.</UiPopover>import { Component } from "@angular/core";
import { UiButton, UiPopover } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiButton, UiPopover],
template: `
<ng-template
ui-button
[variant]="'ghost'"
[hnAttributes]="{ id: 'popover-trigger' }"
>Office briefing</ng-template>
<span
ui-popover
id="example-popover"
[for]="'popover-trigger'"
>Sam has booked the quiet room. Frodo is bringing the maps.</span>
`,
})
export default class Example {}placement="top-end"
Placement sets the preferred edge and alignment against the trigger; here the surface opens above, aligned to the trigger's end edge.
Popover content.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-popover/ui-popover.html"
>
<ui-button
id="details-trigger"
>Open popover</ui-button>
<ui-popover
id="details-popover"
for="details-trigger"
placement="top-end"
>
<strong>Popover heading</strong>
<p>Popover content.</p>
</ui-popover><script setup>
import { UiButton, UiPopover } from "@threadlabs/looma";
</script>
<template>
<UiButton
id="details-trigger"
>Open popover</UiButton>
<UiPopover
id="details-popover"
for="details-trigger"
placement="top-end"
>
<strong>Popover heading</strong>
<p>Popover content.</p>
</UiPopover>
</template>import { UiButton, UiPopover } from "@threadlabs/looma";
export default function Example() {
return <>
<UiButton
id="details-trigger"
>Open popover</UiButton>
<UiPopover
id="details-popover"
htmlFor="details-trigger"
placement="top-end"
>
<strong>Popover heading</strong>
<p>Popover content.</p>
</UiPopover>
</>;
}/** @jsxImportSource preact */
import { UiButton, UiPopover } from "@threadlabs/looma";
export default function Example() {
return <>
<UiButton
id="details-trigger"
>Open popover</UiButton>
<UiPopover
id="details-popover"
htmlFor="details-trigger"
placement="top-end"
>
<strong>Popover heading</strong>
<p>Popover content.</p>
</UiPopover>
</>;
}<script>
import { UiButton, UiPopover } from "@threadlabs/looma";
</script>
<UiButton
id="details-trigger"
>Open popover</UiButton>
<UiPopover
id="details-popover"
for="details-trigger"
placement="top-end"
>
<strong>Popover heading</strong>
<p>Popover content.</p>
</UiPopover>import { Component } from "@angular/core";
import { UiButton, UiPopover } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiButton, UiPopover],
template: `
<ng-template
ui-button
[hnAttributes]="{ id: 'details-trigger' }"
>Open popover</ng-template>
<span
ui-popover
id="details-popover"
[for]="'details-trigger'"
[placement]="'top-end'"
>
<strong>Popover heading</strong>
<p>Popover content.</p>
</span>
`,
})
export default class Example {}Side placement and offset
Use a side placement and a local offset when one popover needs more space from its trigger.
This surface prefers the right side and flips when it cannot fit there.
Code
<link
rel="component"
href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
rel="component"
href="@threadlabs/looma/components/ui-popover/ui-popover.html"
>
<ui-button
id="side-popover-trigger"
>Open details</ui-button>
<ui-popover
for="side-popover-trigger"
placement="right-start"
style="--ui-popover-offset: 0.75rem"
>
<strong>Details</strong>
<p>This surface prefers the right side and flips when it cannot fit there.</p>
</ui-popover><script setup>
import { UiButton, UiPopover } from "@threadlabs/looma";
</script>
<template>
<UiButton
id="side-popover-trigger"
>Open details</UiButton>
<UiPopover
for="side-popover-trigger"
placement="right-start"
style="--ui-popover-offset: 0.75rem"
>
<strong>Details</strong>
<p>This surface prefers the right side and flips when it cannot fit there.</p>
</UiPopover>
</template>import { UiButton, UiPopover } from "@threadlabs/looma";
export default function Example() {
return <>
<UiButton
id="side-popover-trigger"
>Open details</UiButton>
<UiPopover
htmlFor="side-popover-trigger"
placement="right-start"
style={{"--ui-popover-offset":"0.75rem"}}
>
<strong>Details</strong>
<p>This surface prefers the right side and flips when it cannot fit there.</p>
</UiPopover>
</>;
}/** @jsxImportSource preact */
import { UiButton, UiPopover } from "@threadlabs/looma";
export default function Example() {
return <>
<UiButton
id="side-popover-trigger"
>Open details</UiButton>
<UiPopover
htmlFor="side-popover-trigger"
placement="right-start"
style={{"--ui-popover-offset":"0.75rem"}}
>
<strong>Details</strong>
<p>This surface prefers the right side and flips when it cannot fit there.</p>
</UiPopover>
</>;
}<script>
import { UiButton, UiPopover } from "@threadlabs/looma";
</script>
<UiButton
id="side-popover-trigger"
>Open details</UiButton>
<UiPopover
for="side-popover-trigger"
placement="right-start"
style="--ui-popover-offset: 0.75rem"
>
<strong>Details</strong>
<p>This surface prefers the right side and flips when it cannot fit there.</p>
</UiPopover>import { Component } from "@angular/core";
import { UiButton, UiPopover } from "@threadlabs/looma";
@Component({
selector: "example-view",
standalone: true,
imports: [UiButton, UiPopover],
template: `
<ng-template
ui-button
[hnAttributes]="{ id: 'side-popover-trigger' }"
>Open details</ng-template>
<span
ui-popover
[for]="'side-popover-trigger'"
[placement]="'right-start'"
style="--ui-popover-offset: 0.75rem"
>
<strong>Details</strong>
<p>This surface prefers the right side and flips when it cannot fit there.</p>
</span>
`,
})
export default class Example {}API
Package: @threadlabs/looma · Native root: span
Attributes
| Name | Description | Property | Type | Default | Allowed values |
|---|---|---|---|---|---|
bare | Delegates the surface to its content, with no padding, border, background, shadow, or overflow clipping. | bare | boolean | false | — |
for | ID of the control that toggles and anchors the popover. | for | string | | — |
open | Whether the popover is open initially or when set externally. | open | boolean | false | — |
placement | Positions the surface around its trigger. Start and end on top or bottom follow the writing direction. | placement | keyword | bottom-start | top, top-start, top-end, bottom, bottom-start, bottom-end, left, left-start, left-end, right, right-start, right-end |
size | Optional size preset. content keeps the existing intrinsic sizing; the other sizes constrain the surface for compact, medium, or large content. Shared surface separation adapts to popover purpose and rendered size. | size | keyword | content | content, sm, md, lg |
trigger | Click toggles the popover from its for control. Manual retains anchoring, aria-expanded, Escape and light dismissal while the caller controls open. | trigger | keyword | click | click, manual |
Framework props
| Name | Type | Default | Allowed values |
|---|---|---|---|
bare | boolean | false | — |
for | string | | — |
open | boolean | false | — |
placement | "top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end" | bottom-start | top, top-start, top-end, bottom, bottom-start, bottom-end, left, left-start, left-end, right, right-start, right-end |
size | "content" | "sm" | "md" | "lg" | content | content, sm, md, lg |
trigger | "click" | "manual" | click | click, manual |
Events
| Name | Detail | Description |
|---|---|---|
close | object
| Emitted when the component transitions to a closed state. |
open | object
| Emitted when the component transitions to an open state. |
Slots
| Name | Purpose |
|---|---|
default | Default child content. |
Design tokens
Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.
Component tokens
| Token | Declared values | Fallback |
|---|---|---|
--ui-popover-border | — | var(--ui-overlay-border, var(--ui-border-strong)) | color-mix(in oklab, var(--ui-overlay-border, var(--ui-border-strong)) calc((1 - var(--_ui-material-active)) * 100%), transparent) |
--ui-popover-padding | — | var(--ui-space-3) |
--ui-popover-radius | — | var(--ui-overlay-radius, var(--ui-radius-lg)) |
--ui-popover-shadow | — | var(--ui-overlay-shadow, var(--ui-elevation-overlay, var(--ui-shadow-lg))) | var(
--ui-overlay-shadow,
0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 0.55) * 8px) calc(
var(--_ui-material-separation) * var(--_ui-material-scale, 0.55) * 24px
) color-mix(in oklab, var(--ui-text-primary) 15%, transparent),
var(--ui-material-glow, 0 0 transparent),
inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)),
var(--ui-elevation-overlay, var(--ui-shadow-lg))
) |
--ui-popover-surface | — | var(--ui-overlay-surface, var(--ui-surface-elevated)) | var(--_ui-material-underpaint, var(--ui-material-content, var(--ui-overlay-surface, var(--ui-surface-elevated)))) |
--ui-popover-viewport-gap | — | var(--ui-space-2) |
Shared tokens
| Token | Fallback |
|---|---|
--ui-border-strong | — |
--ui-border-width | — |
--ui-elevation-overlay | var(--ui-shadow-lg) |
--ui-glassiness | 0 |
--ui-material-content | var(--ui-overlay-surface, var(--ui-surface-elevated)) |
--ui-material-edge | var(--ui-border-strong) |
--ui-material-glass | var(--ui-surface-elevated) |
--ui-material-glow | 0 0 transparent |
--ui-material-highlight | var(--ui-surface-highlight, linear-gradient(transparent, transparent)) |
--ui-motion-ease | — |
--ui-motion-fast | — |
--ui-overlay-border | var(--ui-border-strong) |
--ui-overlay-radius | var(--ui-radius-lg) |
--ui-overlay-shadow | var(--ui-elevation-overlay, var(--ui-shadow-lg)) | 0 calc(var(--_ui-material-separation) * var(--_ui-material-scale, 0.55) * 8px) calc(
var(--_ui-material-separation) * var(--_ui-material-scale, 0.55) * 24px
) color-mix(in oklab, var(--ui-text-primary) 15%, transparent),
var(--ui-material-glow, 0 0 transparent),
inset 0 0 0 calc(var(--_ui-material-active) * var(--ui-border-width)) var(--ui-material-edge, var(--ui-border-strong)),
var(--ui-elevation-overlay, var(--ui-shadow-lg)) |
--ui-overlay-surface | var(--ui-surface-elevated) |
--ui-radius-lg | — |
--ui-shadow-lg | — |
--ui-space-2 | — |
--ui-space-3 | — |
--ui-surface-elevated | — |
--ui-surface-highlight | linear-gradient(transparent, transparent) |
--ui-surface-separation | 0 |
--ui-text-primary | — |