Skip to main content

Popovers

Floating panels anchored to a trigger button, built on the native HTML popover attribute. Showing, hiding, light-dismiss (outside click), and Escape-to-close are all handled by the browser; the l-ui--popover Stimulus controller only computes placement, flipping to the opposite side if it would overflow the viewport.

CSS-classes example

Use the l-ui-popover class directly when you need full control over markup. Pair a trigger's popovertarget attribute with a popover-attribute element.

Body content. Dismiss by pressing Esc or clicking outside.

<div data-controller="l-ui--popover">
  <button type="button" class="l-ui-button l-ui-button--outline"
          popovertarget="my-popover"
          data-l-ui--popover-target="trigger">
    Open popover
  </button>
  <div id="my-popover" popover="auto" class="l-ui-popover"
       data-l-ui--popover-target="popover">
    Body content.
  </div>
</div>

Helper

The l_ui_popover helper renders the trigger, popover element, and Stimulus wiring for you. The block's content is the popover body. Use <% (not <%=) when calling p.trigger - the builder captures its content.

Rendered by l_ui_popover. Same markup, less boilerplate.

<%= l_ui_popover do |p| %>
  <% p.trigger(class: "l-ui-button l-ui-button--outline") do %>
    Open popover
  <% end %>
  <p>Body content.</p>
<% end %>

Placement

Pass placement: as :top, :bottom (default), :left, or :right. The popover flips to the opposite side automatically if it would overflow the viewport.

Placed above the trigger.

Placed to the right of the trigger.

Placed below the trigger.

Placed to the left of the trigger.

Alignment

Pass align: as :start (default) or :end to choose which edge of the popover flushes with the trigger on the cross axis. align: :end suits a trigger sitting at the end of a row: the popover hangs back over the trigger instead of overflowing past it.

Right edge flush with the trigger's right edge.

<%= l_ui_popover(align: :end) do |p| %>
  <% p.trigger(class: "l-ui-button l-ui-button--outline") do %>
    End-aligned
  <% end %>
  <p>Right edge flush with the trigger's right edge.</p>
<% end %>

Open on connect

Pass open: true to open the popover as soon as its Stimulus controller connects - for example, re-opening a filter popover after a form submission re-renders the page. The controller shows and positions the popover in the same task, so it never paints unpositioned. As an auto popover it still light-dismisses as usual, so the example below is only open until you click elsewhere; reload the page to see it again.

This popover was open when the page loaded.

<%# e.g. open: params[:filtering].present? %>
<%= l_ui_popover(open: true) do |p| %>
  <% p.trigger(class: "l-ui-button l-ui-button--outline") do %>
    Opened on page load
  <% end %>
  <p>This popover was open when the page loaded.</p>
<% end %>

Menu content

For a list of actions (e.g. a "more" menu on a table row), use l-ui-popover__menu to reset the popover's padding to a full-width list, and l-ui-popover__menu-item on each link or button. Add l-ui-popover__menu-item--danger for destructive actions, and an <hr class="l-ui-popover__menu-divider"> to separate groups of items. Give an icon-only trigger an aria-label. Pair with align: :end when the trigger sits at the end of a row.

Edit Duplicate
<%= l_ui_popover(align: :end) do |p| %>
  <% p.trigger(class: "l-ui-button l-ui-button--outline l-ui-button--icon l-ui-button--small",
               "aria-label" => "More actions") do %>
    <%= image_tag "layered_ui/icon_more.svg", alt: "",
          class: "l-ui-icon l-ui-icon--sm", aria: { hidden: true } %>
  <% end %>
  <div class="l-ui-popover__menu">
    <%= link_to "Edit", edit_path, class: "l-ui-popover__menu-item" %>
    <%= link_to "Duplicate", duplicate_path, class: "l-ui-popover__menu-item" %>
    <hr class="l-ui-popover__menu-divider">
    <button type="button" class="l-ui-popover__menu-item l-ui-popover__menu-item--danger">
      Delete
    </button>
  </div>
<% end %>

See Tables for this pattern wired up as a row action menu.

Helper options

l_ui_popover options
Option Description
id: DOM id for the popover element; defaults to an auto-generated id
placement: :top, :bottom (default), :left, or :right. Flips automatically if it would overflow the viewport
align: :start (default) or :end - which edge of the popover flushes with the trigger on the cross axis
open: When true, the popover opens as soon as its Stimulus controller connects, shown and positioned in the same task so it never paints unpositioned; defaults to false
container: Extra HTML attributes for the wrapping <div> (e.g. class:)
p.trigger(**opts) Renders the trigger <button>; opts are merged as HTML attributes
block content The block's output (anything outside p.trigger) is rendered inside l-ui-popover
icon-only triggers If the trigger contains only an icon (no visible text), pass aria-label: so it has an accessible name

CSS classes

Popover CSS classes
Class Description
l-ui-popover Applied to the popover-attribute element; positioned by the l-ui--popover Stimulus controller
l-ui-popover__menu Full-width list wrapper for a menu of actions inside a popover
l-ui-popover__menu-item Full-width action link or button inside l-ui-popover__menu
l-ui-popover__menu-item--danger Danger styling for a destructive menu item
l-ui-popover__menu-divider Horizontal rule separating groups of items in l-ui-popover__menu (apply to an <hr>)