On this page
Overlay
A free-form positioned overlay panel powered by the native Popover API. Use it as the building block for custom panels or any positioned floating content.
uwc-overlay is a blank-canvas positioned panel powered by the native
Popover API. It handles
positioning, animation, optional backdrop, Escape key, and outside-click dismissal. Place a trigger in
slot="trigger" and any content in slot="content".
Import
All components
import '@uwc/components';
Selected component (Lit / Angular / Vue)
import { UwcOverlay } from '@uwc/components/overlay';
customElements.define('uwc-overlay', UwcOverlay);
React
import { UwcOverlay } from '@uwc/components/react';
Usage
Lit
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('app-demo')
export class AppDemo extends LitElement {
render() {
return html`
<uwc-overlay placement="bottom-start">
<uwc-button slot="trigger" label="Open"></uwc-button>
<div slot="content">Any content goes here.</div>
</uwc-overlay>
`;
}
}
React
import React from 'react';
import { UwcOverlay } from '@uwc/components/react';
export default function App() {
return (
<UwcOverlay placement="bottom-start">
<uwc-button slot="trigger" label="Open"></uwc-button>
<div slot="content">Any content goes here.</div>
</UwcOverlay>
);
}
Angular
import { Component } from '@angular/core';
import '@uwc/overlay';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-overlay placement="bottom-start">
<uwc-button slot="trigger" label="Open"></uwc-button>
<div slot="content">Any content goes here.</div>
</uwc-overlay>
`,
})
export class AppComponent {}
Vue
import '@uwc/overlay';
export default {
template: `
<uwc-overlay placement="bottom-start">
<uwc-button slot="trigger" label="Open"></uwc-button>
<div slot="content">Any content goes here.</div>
</uwc-overlay>
`,
};
Basic Usage
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('app-demo')
export class AppDemo extends LitElement {
render() {
return html`
<div style="display:flex;gap:1rem;flex-wrap:wrap;">
<uwc-overlay placement="bottom-start">
<uwc-button slot="trigger" label="Basic overlay"></uwc-button>
<div slot="content" style="padding:1rem 1.25rem;min-width:200px;">
<p style="margin:0 0 .5rem;font-weight:600;">Free-form content</p>
<p style="margin:0;color:#6b7280;font-size:.875rem;">Build any UI inside the overlay.</p>
</div>
</uwc-overlay>
<uwc-overlay placement="bottom" backdrop>
<uwc-button slot="trigger" label="With backdrop"></uwc-button>
<div slot="content" style="padding:1.25rem;min-width:220px;">
<p style="margin:0;font-size:.875rem;color:#374151;">Click outside or press <kbd>Escape</kbd> to close.</p>
</div>
</uwc-overlay>
</div>
`;
}
}
import React from 'react';
import { UwcOverlay } from '@uwc/components/react';
export default function App() {
return (
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<UwcOverlay placement="bottom-start">
<uwc-button slot="trigger" label="Basic overlay"></uwc-button>
<div slot="content" style={{ padding: '1rem 1.25rem', minWidth: '200px' }}>
<p style={{ margin: '0 0 .5rem', fontWeight: 600 }}>Free-form content</p>
<p style={{ margin: 0, color: '#6b7280', fontSize: '.875rem' }}>Build any UI inside the overlay.</p>
</div>
</UwcOverlay>
<UwcOverlay placement="bottom" backdrop>
<uwc-button slot="trigger" label="With backdrop"></uwc-button>
<div slot="content" style={{ padding: '1.25rem', minWidth: '220px' }}>
<p style={{ margin: 0, fontSize: '.875rem', color: '#374151' }}>Click outside or press Escape to close.</p>
</div>
</UwcOverlay>
</div>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<div style="display:flex;gap:1rem;flex-wrap:wrap;">
<uwc-overlay placement="bottom-start">
<uwc-button slot="trigger" label="Basic overlay"></uwc-button>
<div slot="content" style="padding:1rem 1.25rem;min-width:200px;">
<p style="margin:0 0 .5rem;font-weight:600;">Free-form content</p>
<p style="margin:0;color:#6b7280;font-size:.875rem;">Build any UI inside the overlay.</p>
</div>
</uwc-overlay>
<uwc-overlay placement="bottom" backdrop>
<uwc-button slot="trigger" label="With backdrop"></uwc-button>
<div slot="content" style="padding:1.25rem;min-width:220px;">
<p style="margin:0;font-size:.875rem;color:#374151;">Click outside or press Escape to close.</p>
</div>
</uwc-overlay>
</div>
`
})
export class AppComponent {}
export default {
template: `
<div style="display:flex;gap:1rem;flex-wrap:wrap;">
<uwc-overlay placement="bottom-start">
<uwc-button slot="trigger" label="Basic overlay"></uwc-button>
<div slot="content" style="padding:1rem 1.25rem;min-width:200px;">
<p style="margin:0 0 .5rem;font-weight:600;">Free-form content</p>
<p style="margin:0;color:#6b7280;font-size:.875rem;">Build any UI inside the overlay.</p>
</div>
</uwc-overlay>
<uwc-overlay placement="bottom" backdrop>
<uwc-button slot="trigger" label="With backdrop"></uwc-button>
<div slot="content" style="padding:1.25rem;min-width:220px;">
<p style="margin:0;font-size:.875rem;color:#374151;">Click outside or press Escape to close.</p>
</div>
</uwc-overlay>
</div>
`
};
Free-form content
Build any UI inside the overlay.
Click outside or press Escape to close.
Basic
Place any trigger in slot="trigger" and content in slot="content". The panel
positions itself relative to the trigger.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('overlay-basic-demo')
export class AppDemo extends LitElement {
render() {
return html`
<uwc-overlay placement="bottom-start">
<uwc-button slot="trigger" label="Open overlay"></uwc-button>
<div slot="content" style="padding:1.25rem;min-width:240px;">
<p style="margin:0 0 .5rem;font-weight:600;font-size:.9375rem;">Custom panel</p>
<p style="margin:0;color:#6b7280;font-size:.875rem;">Place any content here — forms, lists, media, or nested components.</p>
</div>
</uwc-overlay>
`;
}
}
import React from 'react';
import { UwcOverlay } from '@uwc/components/react';
export default function App() {
return (
<UwcOverlay placement="bottom-start">
<uwc-button slot="trigger" label="Open overlay"></uwc-button>
<div slot="content" style={{ padding: '1.25rem', minWidth: '240px' }}>
<p style={{ margin: '0 0 .5rem', fontWeight: 600, fontSize: '.9375rem' }}>Custom panel</p>
<p style={{ margin: 0, color: '#6b7280', fontSize: '.875rem' }}>Place any content here — forms, lists, media, or nested components.</p>
</div>
</UwcOverlay>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-overlay placement="bottom-start">
<uwc-button slot="trigger" label="Open overlay"></uwc-button>
<div slot="content" style="padding:1.25rem;min-width:240px;">
<p style="margin:0 0 .5rem;font-weight:600;font-size:.9375rem;">Custom panel</p>
<p style="margin:0;color:#6b7280;font-size:.875rem;">Place any content here — forms, lists, media, or nested components.</p>
</div>
</uwc-overlay>
`
})
export class AppComponent {}
export default {
template: `
<uwc-overlay placement="bottom-start">
<uwc-button slot="trigger" label="Open overlay"></uwc-button>
<div slot="content" style="padding:1.25rem;min-width:240px;">
<p style="margin:0 0 .5rem;font-weight:600;font-size:.9375rem;">Custom panel</p>
<p style="margin:0;color:#6b7280;font-size:.875rem;">Place any content here — forms, lists, media, or nested components.</p>
</div>
</uwc-overlay>
`
};
Custom panel
Place any content here — forms, lists, media, or nested components.
With backdrop
Add backdrop to dim the page behind the panel. Clicking the backdrop closes it.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('overlay-backdrop-demo')
export class AppDemo extends LitElement {
render() {
return html`
<uwc-overlay placement="bottom" backdrop>
<uwc-button slot="trigger" label="Open with backdrop"></uwc-button>
<div slot="content" style="padding:1.5rem;min-width:280px;max-width:340px;">
<p style="margin:0 0 .75rem;font-weight:600;">Modal-style panel</p>
<p style="margin:0;color:#6b7280;font-size:.875rem;">The backdrop blocks interaction with the rest of the page. Click outside or press <kbd>Escape</kbd> to dismiss.</p>
</div>
</uwc-overlay>
`;
}
}
import React from 'react';
import { UwcOverlay } from '@uwc/components/react';
export default function App() {
return (
<UwcOverlay placement="bottom" backdrop>
<uwc-button slot="trigger" label="Open with backdrop"></uwc-button>
<div slot="content" style={{ padding: '1.5rem', minWidth: '280px', maxWidth: '340px' }}>
<p style={{ margin: '0 0 .75rem', fontWeight: 600 }}>Modal-style panel</p>
<p style={{ margin: 0, color: '#6b7280', fontSize: '.875rem' }}>The backdrop blocks interaction with the rest of the page. Click outside or press Escape to dismiss.</p>
</div>
</UwcOverlay>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-overlay placement="bottom" backdrop>
<uwc-button slot="trigger" label="Open with backdrop"></uwc-button>
<div slot="content" style="padding:1.5rem;min-width:280px;max-width:340px;">
<p style="margin:0 0 .75rem;font-weight:600;">Modal-style panel</p>
<p style="margin:0;color:#6b7280;font-size:.875rem;">The backdrop blocks interaction with the rest of the page. Click outside or press Escape to dismiss.</p>
</div>
</uwc-overlay>
`
})
export class AppComponent {}
export default {
template: `
<uwc-overlay placement="bottom" backdrop>
<uwc-button slot="trigger" label="Open with backdrop"></uwc-button>
<div slot="content" style="padding:1.5rem;min-width:280px;max-width:340px;">
<p style="margin:0 0 .75rem;font-weight:600;">Modal-style panel</p>
<p style="margin:0;color:#6b7280;font-size:.875rem;">The backdrop blocks interaction with the rest of the page. Click outside or press Escape to dismiss.</p>
</div>
</uwc-overlay>
`
};
Modal-style panel
The backdrop blocks interaction with the rest of the page. Click outside or press Escape to dismiss.
Placement
Use any of the 12 placement values: top, bottom,
left, right plus -start / -end variants.
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('overlay-placement-demo')
export class AppDemo extends LitElement {
render() {
return html`
<div style="display:flex;gap:.75rem;flex-wrap:wrap;">
${['bottom-start', 'bottom', 'bottom-end', 'top-start', 'right', 'left'].map(p => html`
<uwc-overlay placement=${p}>
<uwc-button slot="trigger" label=${p}></uwc-button>
<div slot="content" style="padding:.75rem 1rem;font-size:.875rem;">${p}</div>
</uwc-overlay>
`)}
</div>
`;
}
}
import React from 'react';
import { UwcOverlay } from '@uwc/components/react';
export default function App() {
const placements = ['bottom-start', 'bottom', 'bottom-end', 'top-start', 'right', 'left'];
return (
<div style={{ display: 'flex', gap: '.75rem', flexWrap: 'wrap' }}>
{placements.map(p => (
<UwcOverlay key={p} placement={p}>
<uwc-button slot="trigger" label={p}></uwc-button>
<div slot="content" style={{ padding: '.75rem 1rem', fontSize: '.875rem' }}>{p}</div>
</UwcOverlay>
))}
</div>
);
}
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<div style="display:flex;gap:.75rem;flex-wrap:wrap;">
@for (p of placements; track p) {
<uwc-overlay [placement]="p">
<uwc-button slot="trigger" [label]="p"></uwc-button>
<div slot="content" style="padding:.75rem 1rem;font-size:.875rem;">{{ p }}</div>
</uwc-overlay>
}
</div>
`
})
export class AppComponent {
placements = ['bottom-start', 'bottom', 'bottom-end', 'top-start', 'right', 'left'];
}
export default {
data() {
return {
placements: ['bottom-start', 'bottom', 'bottom-end', 'top-start', 'right', 'left'],
};
},
template: `
<div style="display:flex;gap:.75rem;flex-wrap:wrap;">
<uwc-overlay v-for="p in placements" :key="p" :placement="p">
<uwc-button slot="trigger" :label="p"></uwc-button>
<div slot="content" style="padding:.75rem 1rem;font-size:.875rem;">{{ p }}</div>
</uwc-overlay>
</div>
`
};
Listening to events
uwc-show and uwc-hide fire when the panel opens and closes.
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
@customElement('overlay-events-demo')
export class AppDemo extends LitElement {
static styles = css`
:host { display: block; }
.log { margin-top:.75rem;padding:.6rem .75rem;border-radius:6px;background:#f9fafb;border:1px solid #e5e7eb;font-size:.8rem;font-family:monospace;min-height:2.5rem;color:#374151; }
`;
@state() private _log: string[] = [];
private _record(event: string) {
const time = new Date().toLocaleTimeString();
this._log = [`[${time}] ${event}`, ...this._log].slice(0, 5);
}
render() {
return html`
<uwc-overlay
placement="bottom-start"
@uwc-show=${() => this._record('uwc-show')}
@uwc-hide=${() => this._record('uwc-hide')}>
<uwc-button slot="trigger" label="Toggle overlay"></uwc-button>
<div slot="content" style="padding:1rem;min-width:200px;font-size:.875rem;">Open and close me to see events.</div>
</uwc-overlay>
<div class="log">
${this._log.length
? this._log.map(l => html`<div>${l}</div>`)
: html`<span style="color:#9ca3af">Open/close the overlay to see events...</span>`}
</div>
`;
}
}
import React, { useState } from 'react';
import { UwcOverlay } from '@uwc/components/react';
export default function App() {
const [log, setLog] = useState([]);
const record = (event) => {
const time = new Date().toLocaleTimeString();
setLog(prev => [`[${time}] ${event}`, ...prev].slice(0, 5));
};
return (
<>
<UwcOverlay
placement="bottom-start"
onUwcShow={() => record('uwc-show')}
onUwcHide={() => record('uwc-hide')}
>
<uwc-button slot="trigger" label="Toggle overlay"></uwc-button>
<div slot="content" style={{ padding: '1rem', minWidth: '200px', fontSize: '.875rem' }}>Open and close me to see events.</div>
</UwcOverlay>
<div style={{ marginTop: '.75rem', fontFamily: 'monospace', fontSize: '.8rem' }}>
{log.length
? log.map((l, i) => <div key={i}>{l}</div>)
: <span style={{ color: '#9ca3af' }}>Open/close the overlay to see events...</span>}
</div>
</>
);
}
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
template: `
<uwc-overlay
placement="bottom-start"
(uwc-show)="record('uwc-show')"
(uwc-hide)="record('uwc-hide')">
<uwc-button slot="trigger" label="Toggle overlay"></uwc-button>
<div slot="content" style="padding:1rem;min-width:200px;font-size:.875rem;">Open and close me to see events.</div>
</uwc-overlay>
<div style="margin-top:.75rem;font-family:monospace;font-size:.8rem;">
@for (l of log(); track l) {
<div>{{ l }}</div>
}
@if (!log().length) {
<span style="color:#9ca3af">Open/close the overlay to see events...</span>
}
</div>
`
})
export class AppComponent {
readonly log = signal<string[]>([]);
record(event: string) {
const time = new Date().toLocaleTimeString();
this.log.set([`[${time}] ${event}`, ...this.log()].slice(0, 5));
}
}
export default {
data() {
return { log: [] };
},
methods: {
record(event) {
const time = new Date().toLocaleTimeString();
this.log = [`[${time}] ${event}`, ...this.log].slice(0, 5);
},
},
template: `
<uwc-overlay
placement="bottom-start"
@uwc-show="record('uwc-show')"
@uwc-hide="record('uwc-hide')">
<uwc-button slot="trigger" label="Toggle overlay"></uwc-button>
<div slot="content" style="padding:1rem;min-width:200px;font-size:.875rem;">Open and close me to see events.</div>
</uwc-overlay>
<div style="margin-top:.75rem;font-family:monospace;font-size:.8rem;">
<div v-for="l in log" :key="l">{{ l }}</div>
<span v-if="!log.length" style="color:#9ca3af">Open/close the overlay to see events...</span>
</div>
`
};
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
trigger-id |
— |
— |
ID of an external trigger element. Supports deep shadow-DOM traversal. |
placement |
— |
— |
Panel placement relative to the trigger. One of: top | top-start | top-end | bottom | bottom-start | bottom-end | left | left-start | left-end | right | right-start | right-end |
offset |
— |
— |
Gap in px between trigger and panel. Default: 8. |
backdrop |
— |
— |
Show a semi-transparent backdrop behind the panel. |
close-on-escape |
— |
— |
Close on Escape key. Default: true. |
close-on-outside-click |
— |
— |
Close on click outside. Default: true. |
Properties
| Name | Type | Default | Description |
|---|---|---|---|
triggerId |
string | undefined |
— |
— |
placement |
Placement |
'bottom' |
— |
offset |
number |
8 |
— |
backdrop |
boolean |
false |
— |
closeOnEscape |
boolean |
true |
— |
closeOnOutsideClick |
boolean |
true |
— |
isOpen |
boolean |
— |
— |
styles |
array |
[styles] |
— |
Slots
| Name | Description |
|---|---|
trigger |
The element that opens the overlay on click. Omit when using trigger-id for external triggers. |
content |
Main overlay content. |
(default) |
Fallback content slot (external-trigger pattern). |
Events
| Name | Type | Description |
|---|---|---|
uwc-show |
CustomEvent |
Fired when the panel opens. |
uwc-hide |
CustomEvent |
Fired when the panel closes. |
CSS Custom Properties
| Name | Default | Description |
|---|---|---|
--uwc-overlay-bg |
— |
Panel background. Default: #fff. |
--uwc-overlay-border |
— |
Panel border. |
--uwc-overlay-radius |
— |
Panel border-radius. Default: 10px. |
--uwc-overlay-shadow |
— |
Panel box-shadow. |
--uwc-overlay-z |
— |
Panel z-index. Default: 9999. |
--uwc-overlay-duration |
— |
Open/close transition duration. Default: 160ms. |
--uwc-backdrop-color |
— |
Backdrop background. Default: rgba(0,0,0,0.25). |
CSS Parts
| Name | Description |
|---|---|
trigger |
The trigger slot wrapper. |
panel |
The overlay panel element. |
backdrop |
The backdrop element. |