A wider screen, please

This page features interactive demos and side-by-side code samples that need at least 340 pixels of horizontal space. Please widen your browser window, or rotate your device, to continue.

Morpheus v0.1.0
v0.1.0

Popover#

Component <neo-popover> is an anchored floating panel attached to a trigger, with a [data-neo-popover-trigger] child and a [data-neo-popover-content] panel. The host owns positioning, the open/close events, outside-click and Esc dismissal, and the strict-placement clamp logic.

Default
Open
Placements
With actions
Tall content (clamps + scrolls)
Strict placement
Wide content (horizontal clamp)
Hover to open
Deactivate Rename Duplicate
Delete
New state Toggle autoplay
Open popover

Hello

This is a basic popover. Press Esc or click outside to close.

HTML CSS

The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.

Replace Patch

Signals

Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.

Patch

Reference#

Attributes #

NameTypeDefaultDescription
openboolean reflected observedfalseControls panel visibility.
trigger-action"show" | "hide"toggleWhat clicking the trigger does. Default toggles open and closed; show only opens (for hover-then-click patterns like submenus, where a click must not close an already-open panel); hide only closes.
placement"bottom-start" | "bottom" | "bottom-end" | "top-start" | "top" | "top-end" | "left-start" | "left" | "left-end" | "right-start" | "right" | "right-end" observed"bottom-start"Anchor position relative to the trigger.
screen-offsetCSS length observed8pxMargin kept clear of the viewport edges. Mirrors to --neo-popover-screen-offset.
follow-scroll"always" | "until-trigger-invisible"unsetWhen absent, close on outside scroll. Set to always to stay anchored to the trigger during scroll. Set to until-trigger-invisible to stay anchored until the trigger leaves the viewport, then close.
clamp-placementboolean observedfalseOpt out of the default strict placement. The panel position re-anchors to keep itself inside the viewport when content would otherwise overflow, instead of staying glued to the configured side and shrinking max-width/max-height.
min-fit-heightCSS length | "content" observed"content"Minimum vertical slot the strict placement requires. If the slot at the configured side is shorter and the opposite side has more room, the panel auto-flips (e.g. bottom-start -> top-start). content resolves from the panel's scroll height on each positioning pass. Mirrors to --neo-popover-min-fit-height.
min-fit-widthCSS length | "content" observed"content"Same as min-fit-height but for the horizontal axis (left-*/right-*). content resolves from the panel's scroll width. Use 0 to disable the horizontal flip. Mirrors to --neo-popover-min-fit-width.
min-open-heightCSS length observed2remMinimum usable vertical slot required to remain open after placement and auto-flip. If the bounded slot is smaller, the popover closes instead of rendering a cramped panel. Use 0 to disable. Mirrors to --neo-popover-min-open-height.
min-open-widthCSS length observed2remSame as min-open-height but for horizontal space. Use 0 to disable. Mirrors to --neo-popover-min-open-width.
fliptrue | false observedtrueAuto-flip to the opposite side when the min-fit-* threshold is not met. Set flip="false" to pin the panel to the configured primary side; if opening would then make it too cramped, the popover scrolls the trigger into view and opens immediately.
match-trigger-widthboolean observedfalseSize the panel's border-box width to the trigger's border-box width on every positioning pass. Used by neo-select and neo-combobox.
hoverbooleanfalseOpen on pointer hover over the trigger; close after the pointer leaves both the trigger and the content. Mouse only; touch falls back to click. While set, clicking the trigger always shows the panel (instantly, no delay) so touch and keyboard users have a way in; click never toggles it shut.
hover-open-delayinteger (ms)100Milliseconds the pointer must rest on the trigger before hover opens the panel. Filters out incidental cursor crossings. Does not apply to click.
hover-close-delayinteger (ms)200Milliseconds to wait after the pointer leaves both trigger and content before hover closes the panel. Long enough to bridge the gap between trigger and panel without snapping shut.

Slots #

NameDescription
[data-neo-popover-trigger]Click target that opens the popover.
[data-neo-popover-content]Floating panel content.
[data-neo-popover-close]Element inside the content that closes the popover on click (matched via closest, so a descendant works too).

Events #

NameDetailBubblesDescription
neo-popover-opennoneyesFires when the popover opens.
neo-popover-closenoneyesFires when the popover closes.
neo-popover-position{ placement: string }yesFires after the panel is positioned; placement is the effective placement after any auto-flip or viewport clamp.

CSS variables #

NameDefaultDescription
--neo-popover-bg#ffffffPanel background. Defaults to a dark surface under prefers-color-scheme: dark.
--neo-popover-color#111827Panel text color. Defaults to a light tone under prefers-color-scheme: dark.
--neo-popover-border-colorrgba(0, 0, 0, 0.08)Panel border color.
--neo-popover-border-width1pxPanel border width.
--neo-popover-radiusvar(--page-radius, 0.25rem)Panel corner radius.
--neo-popover-paddingcalc(var(--page-spacing, 0.25rem) * 4)Inner padding of the panel.
--neo-popover-shadow0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)Panel box shadow.
--neo-popover-min-width14remPanel minimum width.
--neo-popover-max-width22remPanel maximum width.
--neo-popover-z-index1000Panel stacking order.
--neo-popover-enter-durationcalc(140ms * var(--neo-duration-scale, 1))Open/close transition duration.
--neo-popover-screen-offset8pxMinimum margin kept between the panel and the viewport edges when clamped. Overridable per-instance via the screen-offset attribute.

Examples#

Placements#

Twelve combinations: each side (top, right, bottom, left) paired with optional -start or -end alignment. Bare side names center the panel on that edge; bottom-start is the initial value.

top-start
Top-start
top
Top
top-end
Top-end
right-start
Right-start
right
Right
right-end
Right-end
bottom-start
Bottom-start
bottom
Bottom
bottom-end
Bottom-end
left-start
Left-start
left
Left
left-end
Left-end

With actions#

Pair the popover with a confirm / cancel toolbar inside the content panel. The action buttons carry data-neo-popover-close so picking either dismisses the panel.

With actions

Delete this item?

This action can't be undone.

Cancel Delete

Tall content (clamps + scrolls)#

When the panel is taller than the viewport it clamps to the screen (leaving screen-offset of margin) and scrolls internally rather than overflowing the page.

Tall content (clamps + scrolls)

Long list of countries

Panel is taller than the viewport. It clamps to the screen (leaving screen-offset of margin) and scrolls internally.

  • 🇦🇫 Afghanistan
  • 🇦🇱 Albania
  • 🇩🇿 Algeria
  • 🇦🇩 Andorra
  • 🇦🇴 Angola
  • 🇦🇬 Antigua and Barbuda
  • 🇦🇷 Argentina
  • 🇦🇲 Armenia
  • 🇦🇺 Australia
  • 🇦🇹 Austria
  • 🇦🇿 Azerbaijan
  • 🇧🇸 Bahamas
  • 🇧🇭 Bahrain
  • 🇧🇩 Bangladesh
  • 🇧🇧 Barbados
  • 🇧🇾 Belarus
  • 🇧🇪 Belgium
  • 🇧🇿 Belize
  • 🇧🇯 Benin
  • 🇧🇹 Bhutan
  • 🇧🇴 Bolivia
  • 🇧🇦 Bosnia and Herzegovina
  • 🇧🇼 Botswana
  • 🇧🇷 Brazil
  • 🇧🇳 Brunei
  • 🇧🇬 Bulgaria
  • 🇧🇫 Burkina Faso
  • 🇧🇮 Burundi
  • 🇰🇭 Cambodia
  • 🇨🇲 Cameroon
  • 🇨🇦 Canada
  • 🇨🇻 Cape Verde
  • 🇨🇫 Central African Republic
  • 🇹🇩 Chad
  • 🇨🇱 Chile
  • 🇨🇳 China
  • 🇨🇴 Colombia
  • 🇰🇲 Comoros
  • 🇨🇬 Congo
  • 🇨🇷 Costa Rica
  • 🇭🇷 Croatia
  • 🇨🇺 Cuba
  • 🇨🇾 Cyprus
  • 🇨🇿 Czechia
  • 🇩🇰 Denmark
  • 🇩🇯 Djibouti
  • 🇩🇲 Dominica
  • 🇩🇴 Dominican Republic
  • 🇪🇨 Ecuador
  • 🇪🇬 Egypt
  • 🇸🇻 El Salvador
  • 🇬🇶 Equatorial Guinea
  • 🇪🇷 Eritrea
  • 🇪🇪 Estonia
  • 🇸🇿 Eswatini
  • 🇪🇹 Ethiopia
  • 🇫🇯 Fiji
  • 🇫🇮 Finland
  • 🇫🇷 France
  • 🇬🇦 Gabon
  • 🇬🇲 Gambia
  • 🇬🇪 Georgia
  • 🇩🇪 Germany
  • 🇬🇭 Ghana
  • 🇬🇷 Greece
  • 🇬🇩 Grenada
  • 🇬🇹 Guatemala
  • 🇬🇳 Guinea
  • 🇬🇼 Guinea-Bissau
  • 🇬🇾 Guyana
  • 🇭🇹 Haiti
  • 🇭🇳 Honduras
  • 🇭🇺 Hungary
  • 🇮🇸 Iceland
  • 🇮🇳 India
  • 🇮🇩 Indonesia
  • 🇮🇷 Iran
  • 🇮🇶 Iraq
  • 🇮🇪 Ireland
  • 🇮🇱 Israel
  • 🇮🇹 Italy
  • 🇯🇲 Jamaica
  • 🇯🇵 Japan
  • 🇯🇴 Jordan
  • 🇰🇿 Kazakhstan
  • 🇰🇪 Kenya
  • 🇰🇮 Kiribati
  • 🇰🇼 Kuwait
  • 🇰🇬 Kyrgyzstan
  • 🇱🇦 Laos
  • 🇱🇻 Latvia
  • 🇱🇧 Lebanon
  • 🇱🇸 Lesotho
  • 🇱🇷 Liberia
  • 🇱🇾 Libya
  • 🇱🇮 Liechtenstein
  • 🇱🇹 Lithuania
  • 🇱🇺 Luxembourg
  • 🇲🇬 Madagascar
  • 🇲🇼 Malawi
  • 🇲🇾 Malaysia
  • 🇲🇻 Maldives

Strict placement#

Setting flip="false" pins the panel to bottom-start and keeps that side instead of flipping above when the slot runs short. When the bottom slot is shorter than min-fit-height, opening the popover scrolls the trigger into a useful position and shows the panel immediately. Set the clamp-placement attribute to let the panel re-anchor inside the viewport instead. Scroll so the trigger sits near the bottom of the viewport, then open the panel to see it scroll into place.

Strict bottom-start

Long list

  • 🇦🇫 Afghanistan
  • 🇦🇱 Albania
  • 🇩🇿 Algeria
  • 🇦🇩 Andorra
  • 🇦🇴 Angola
  • 🇦🇬 Antigua and Barbuda
  • 🇦🇷 Argentina
  • 🇦🇲 Armenia
  • 🇦🇺 Australia
  • 🇦🇹 Austria
  • 🇦🇿 Azerbaijan
  • 🇧🇸 Bahamas
  • 🇧🇭 Bahrain
  • 🇧🇩 Bangladesh
  • 🇧🇧 Barbados
  • 🇧🇾 Belarus
  • 🇧🇪 Belgium
  • 🇧🇿 Belize
  • 🇧🇯 Benin
  • 🇧🇹 Bhutan
  • 🇧🇴 Bolivia
  • 🇧🇦 Bosnia and Herzegovina
  • 🇧🇼 Botswana
  • 🇧🇷 Brazil
  • 🇧🇳 Brunei
  • 🇧🇬 Bulgaria
  • 🇧🇫 Burkina Faso
  • 🇧🇮 Burundi
  • 🇰🇭 Cambodia
  • 🇨🇲 Cameroon
  • 🇨🇦 Canada
  • 🇨🇻 Cape Verde
  • 🇨🇫 Central African Republic
  • 🇹🇩 Chad
  • 🇨🇱 Chile
  • 🇨🇳 China
  • 🇨🇴 Colombia
  • 🇰🇲 Comoros
  • 🇨🇬 Congo
  • 🇨🇷 Costa Rica
  • 🇭🇷 Croatia
  • 🇨🇺 Cuba
  • 🇨🇾 Cyprus
  • 🇨🇿 Czechia
  • 🇩🇰 Denmark
  • 🇩🇯 Djibouti
  • 🇩🇲 Dominica
  • 🇩🇴 Dominican Republic
  • 🇪🇨 Ecuador
  • 🇪🇬 Egypt
  • 🇸🇻 El Salvador
  • 🇬🇶 Equatorial Guinea
  • 🇪🇷 Eritrea
  • 🇪🇪 Estonia
  • 🇸🇿 Eswatini
  • 🇪🇹 Ethiopia
  • 🇫🇯 Fiji
  • 🇫🇮 Finland
  • 🇫🇷 France
  • 🇬🇦 Gabon
  • 🇬🇲 Gambia
  • 🇬🇪 Georgia
  • 🇩🇪 Germany
  • 🇬🇭 Ghana
  • 🇬🇷 Greece
  • 🇬🇩 Grenada
  • 🇬🇹 Guatemala
  • 🇬🇳 Guinea
  • 🇬🇼 Guinea-Bissau
  • 🇬🇾 Guyana
  • 🇭🇹 Haiti
  • 🇭🇳 Honduras
  • 🇭🇺 Hungary
  • 🇮🇸 Iceland
  • 🇮🇳 India
  • 🇮🇩 Indonesia
  • 🇮🇷 Iran
  • 🇮🇶 Iraq
  • 🇮🇪 Ireland
  • 🇮🇱 Israel
  • 🇮🇹 Italy
  • 🇯🇲 Jamaica
  • 🇯🇵 Japan
  • 🇯🇴 Jordan
  • 🇰🇿 Kazakhstan
  • 🇰🇪 Kenya
  • 🇰🇮 Kiribati
  • 🇰🇼 Kuwait
  • 🇰🇬 Kyrgyzstan
  • 🇱🇦 Laos
  • 🇱🇻 Latvia
  • 🇱🇧 Lebanon
  • 🇱🇸 Lesotho
  • 🇱🇷 Liberia
  • 🇱🇾 Libya
  • 🇱🇮 Liechtenstein
  • 🇱🇹 Lithuania
  • 🇱🇺 Luxembourg
  • 🇲🇬 Madagascar
  • 🇲🇼 Malawi
  • 🇲🇾 Malaysia
  • 🇲🇻 Maldives

Auto-flip threshold#

Strict placement keeps the panel glued to the configured side. When that slot is shorter than min-fit-height (here 8rem; default content) and the opposite side has more room, the primary axis flips: bottom-start becomes top-start. Scroll so the trigger sits near the bottom of the viewport, then open the panel to see it flip.

Open popover

Long list

  • 🇦🇫 Afghanistan
  • 🇦🇱 Albania
  • 🇩🇿 Algeria
  • 🇦🇩 Andorra
  • 🇦🇴 Angola
  • 🇦🇬 Antigua and Barbuda
  • 🇦🇷 Argentina
  • 🇦🇲 Armenia
  • 🇦🇺 Australia
  • 🇦🇹 Austria
  • 🇦🇿 Azerbaijan
  • 🇧🇸 Bahamas
  • 🇧🇭 Bahrain
  • 🇧🇩 Bangladesh
  • 🇧🇧 Barbados
  • 🇧🇾 Belarus
  • 🇧🇪 Belgium
  • 🇧🇿 Belize
  • 🇧🇯 Benin
  • 🇧🇹 Bhutan
  • 🇧🇴 Bolivia
  • 🇧🇦 Bosnia and Herzegovina
  • 🇧🇼 Botswana
  • 🇧🇷 Brazil
  • 🇧🇳 Brunei
  • 🇧🇬 Bulgaria
  • 🇧🇫 Burkina Faso
  • 🇧🇮 Burundi
  • 🇰🇭 Cambodia
  • 🇨🇲 Cameroon
  • 🇨🇦 Canada
  • 🇨🇻 Cape Verde
  • 🇨🇫 Central African Republic
  • 🇹🇩 Chad
  • 🇨🇱 Chile
  • 🇨🇳 China
  • 🇨🇴 Colombia
  • 🇰🇲 Comoros
  • 🇨🇬 Congo
  • 🇨🇷 Costa Rica
  • 🇭🇷 Croatia
  • 🇨🇺 Cuba
  • 🇨🇾 Cyprus
  • 🇨🇿 Czechia
  • 🇩🇰 Denmark
  • 🇩🇯 Djibouti
  • 🇩🇲 Dominica
  • 🇩🇴 Dominican Republic
  • 🇪🇨 Ecuador
  • 🇪🇬 Egypt
  • 🇸🇻 El Salvador
  • 🇬🇶 Equatorial Guinea
  • 🇪🇷 Eritrea
  • 🇪🇪 Estonia
  • 🇸🇿 Eswatini
  • 🇪🇹 Ethiopia
  • 🇫🇯 Fiji
  • 🇫🇮 Finland
  • 🇫🇷 France
  • 🇬🇦 Gabon
  • 🇬🇲 Gambia
  • 🇬🇪 Georgia
  • 🇩🇪 Germany
  • 🇬🇭 Ghana
  • 🇬🇷 Greece
  • 🇬🇩 Grenada
  • 🇬🇹 Guatemala
  • 🇬🇳 Guinea
  • 🇬🇼 Guinea-Bissau
  • 🇬🇾 Guyana
  • 🇭🇹 Haiti
  • 🇭🇳 Honduras
  • 🇭🇺 Hungary
  • 🇮🇸 Iceland
  • 🇮🇳 India
  • 🇮🇩 Indonesia
  • 🇮🇷 Iran
  • 🇮🇶 Iraq
  • 🇮🇪 Ireland
  • 🇮🇱 Israel
  • 🇮🇹 Italy
  • 🇯🇲 Jamaica
  • 🇯🇵 Japan
  • 🇯🇴 Jordan
  • 🇰🇿 Kazakhstan
  • 🇰🇪 Kenya
  • 🇰🇮 Kiribati
  • 🇰🇼 Kuwait
  • 🇰🇬 Kyrgyzstan
  • 🇱🇦 Laos
  • 🇱🇻 Latvia
  • 🇱🇧 Lebanon
  • 🇱🇸 Lesotho
  • 🇱🇷 Liberia
  • 🇱🇾 Libya
  • 🇱🇮 Liechtenstein
  • 🇱🇹 Lithuania
  • 🇱🇺 Luxembourg
  • 🇲🇬 Madagascar
  • 🇲🇼 Malawi
  • 🇲🇾 Malaysia
  • 🇲🇻 Maldives

Wide content (horizontal clamp)#

When the panel is wider than the viewport it clamps to the screen (leaving screen-offset of margin) and scrolls horizontally inside the panel. The horizontal clamp mirrors the vertical one.

Wide content (horizontal clamp)

Browse

This panel is wider than the viewport. It clamps to the screen (leaving screen-offset of margin) and scrolls horizontally.

Products

  • Dashboard
  • Analytics
  • Commerce
  • CRM
  • Billing

Solutions

  • For startups
  • For agencies
  • For enterprise
  • For developers
  • For education

Resources

  • Docs
  • Guides
  • Blog
  • Changelog
  • API reference

Templates

  • Landing page
  • Portfolio
  • SaaS starter
  • Admin panel
  • E-commerce

Developers

  • CLI
  • SDK
  • REST API
  • GraphQL
  • Webhooks

Community

  • Forum
  • Discord
  • GitHub
  • Stack Overflow
  • YouTube

Hover to open#

Set hover on the host and the panel opens when the pointer enters the trigger, closing after the pointer leaves both the trigger and the panel. The hover-open-delay filters incidental cursor crossings; hover-close-delay gives the pointer time to bridge the gap from trigger to panel before the close kicks in. Click still works (instantly, no delay) so touch and keyboard users have a way in; clicking a hover popover never toggles it shut.

Hover or click me

Hover-opened

Move the pointer away to dismiss. The panel waits hover-close-delay ms first so a short trip through the gap doesn't close it.

Examples: Datastar#

Morphing during interaction#

A Datastar app can fat-morph the page at any moment, so a <neo-popover>'s panel content can change while it is open. Open the popover and watch autoplay push progress into it: the panel stays open and its content reconciles in place because the host element stays mounted across the morph, with no open attribute needed.

Initial
Progress: 50%
Progress: 75%
Progress: 100%
Deactivate Rename Duplicate
Delete
New state Toggle autoplay
Show upload

Preparing…

HTML CSS

The selected state's markup and its scoped CSS. The CSS is wrapped in <style>@scope { ... }</style> and applies to the preview only. Edits change the preview above, applied live while Sync is on, or when you click Patch or Replace. Patch morphs the preview; Replace swaps it outright.

Replace Patch

Signals

Datastar signals declared in the selected state's markup. Change a value to drive the preview, applied live while Sync is on, or when you click Patch.

Patch

Cascading popovers#

Popovers nest naturally. Each child popover lives inside its parent's content panel, so a click inside any open level keeps every ancestor open and a click outside closes the whole chain. Esc closes one level at a time. Submenu triggers are regular menu items; non-submenu items carry data-neo-popover-close so picking one collapses the entire stack. Hovering a submenu trigger opens it; hovering a sibling closes any open submenu at that level.

File
New fileOpen recent
Project A.json Project B.json Notes.md
SaveSettings
EditorTheme
Light Dark System
Keybindings
Quit

Lazy-loaded content#

A popover whose body is fetched on first open. Wire data-on:neo-popover-open to a loader that morphs the rendered content into the placeholder.

  • The loader fires once per open; bind requestCancellation to a fresh AbortController so the request can be dropped.
  • neo-popover-close aborts the in-flight request, so closing mid-load leaves no stale content. Open the popover to see it.

Loading states only show with simulated delay. Raise network latency or handler delay in the Settings panel.

Open lazy-loaded popover
Loading…

Async load with failure swap#

A popover that fetches its body on open, shows a loading placeholder while the request is in flight, and swaps in a failure placeholder once Datastar exhausts its retry budget (it dispatches datastar-fetch with type: 'retries-failed'). On success the server morphs the body in place.

The demo retries with retry: auto, which covers only network throws. Use the server unreachable control to force one: Datastar exhausts the retry budget, fires retries-failed, and the failure placeholder swaps in. A server response status error is not retried by auto, so the swap falls back to finished-after-error. Also see Datastar action options.

Loading states only show with simulated delay. Raise network latency or handler delay in the Settings panel.

Open async popover
Loading options…

Settings

Theme

Default Sunshine Ocean Matrix Rose

Accessibility

Playgrounds

Server Simulator