UWC Components
  • Default
  • Material
  • Fluent

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.