Popover

Displays rich content in a portal, triggered by a button.

Stimulus: shadcn--popover
popover/default
<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

Installation

Add the component to your project:

rails generate shadcn:add popover

Usage

<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

Examples

Basic

<%= render Shadcn::PopoverComponent.new do |popover| %>
  <% popover.with_trigger do %>
    <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Click me" } %>
  <% end %>
  <% popover.with_body do %>
    <div class="grid gap-4">
      <h4 class="font-medium leading-none">Basic Popover</h4>
      <p class="text-sm text-muted-foreground">
        This is a simple popover with some text content.
      </p>
    </div>
  <% end %>
<% end %>

Positions

Use the side prop to control the popover position.

<div class="flex flex-wrap items-center justify-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :top) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Top" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Top positioned popover</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :right) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Right" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Right positioned popover</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Bottom" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Bottom positioned popover</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :left) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Left" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Left positioned popover</p>
    <% end %>
  <% end %>
</div>

Alignment

Use the align prop to control the alignment relative to the trigger.

<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

With Form Content

Popovers can contain interactive form elements.

<%= render Shadcn::PopoverComponent.new do |popover| %>
  <% popover.with_trigger do %>
    <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Settings" } %>
  <% end %>
  <% popover.with_body do %>
    <div class="grid gap-4">
      <div class="space-y-2">
        <h4 class="font-medium leading-none">Settings</h4>
        <p class="text-sm text-muted-foreground">
          Configure your preferences.
        </p>
      </div>
      <div class="grid gap-3">
        <div class="grid gap-2">
          <%= render Shadcn::LabelComponent.new(for: "name") { "Name" } %>
          <%= render Shadcn::InputComponent.new(id: "name", placeholder: "Enter your name") %>
        </div>
        <div class="grid gap-2">
          <%= render Shadcn::LabelComponent.new(for: "email") { "Email" } %>
          <%= render Shadcn::InputComponent.new(id: "email", type: "email", placeholder: "Enter your email") %>
        </div>
        <div class="flex items-center gap-2">
          <%= render Shadcn::CheckboxComponent.new(id: "notifications") %>
          <%= render Shadcn::LabelComponent.new(for: "notifications") { "Enable notifications" } %>
        </div>
        <%= render Shadcn::ButtonComponent.new(size: :sm, class: "mt-2") { "Save changes" } %>
      </div>
    </div>
  <% end %>
<% end %>

Modal Mode

When modal is true, interaction with the rest of the page is blocked.

<%= render Shadcn::PopoverComponent.new(modal: true) do |popover| %>
  <% popover.with_trigger do %>
    <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open modal popover" } %>
  <% end %>
  <% popover.with_body do %>
    <div class="grid gap-4">
      <h4 class="font-medium leading-none">Modal Popover</h4>
      <p class="text-sm text-muted-foreground">
        This popover blocks interaction with the rest of the page until closed.
      </p>
      <p class="text-sm text-muted-foreground">
        Press ESC or click outside to close.
      </p>
    </div>
  <% end %>
<% end %>

Initially Open

Set open: true to show the popover by default.

<%= render Shadcn::PopoverComponent.new(open: true) do |popover| %>
  <% popover.with_trigger do %>
    <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Already open" } %>
  <% end %>
  <% popover.with_body do %>
    <p class="text-sm">This popover is open by default</p>
  <% end %>
<% end %>

All Sides

popover/sides
<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

All Alignments

popover/alignments
<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

Simple Content

popover/simple
<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

With Form

popover/with_form
<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

Top Position

popover/top
<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

Right Position

popover/right
<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

Left Position

popover/left
<div class="flex flex-wrap items-center gap-4">
  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :start) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Start" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the start of trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :center) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align Center" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Centered on trigger</p>
    <% end %>
  <% end %>

  <%= render Shadcn::PopoverComponent.new(side: :bottom, align: :end) do |popover| %>
    <% popover.with_trigger do %>
      <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Align End" } %>
    <% end %>
    <% popover.with_body do %>
      <p class="text-sm">Aligned to the end of trigger</p>
    <% end %>
  <% end %>
</div>

API Reference

PopoverComponent

API Reference

Prop Type Default Description
open Boolean false Whether the popover is open by default
side Symbol :bottom Side to position the popover (:top, :right, :bottom, :left)
align Symbol :center Alignment relative to trigger (:start, :center, :end)
modal Boolean false Whether to trap focus and block interaction with the rest of the page
class_name String nil Additional CSS classes to apply to the popover wrapper

Slots

API Reference

Prop Type Default Description
trigger Block nil The button or element that triggers the popover
body Block nil The content to display in the popover

Stimulus Controller

This component requires JavaScript. The Stimulus controller shadcn--popover provides interactivity.

Handles popover open/close state, positioning, and click-outside behavior.

Installation

Add to your config/importmap.rb:

pin "shadcn", to: "index.esm.js"

Then in your app/javascript/controllers/index.js:

import { Application } from "@hotwired/stimulus"
import { registerShadcnControllers } from "shadcn"

const application = Application.start()
registerShadcnControllers(application)

Or import just this controller:

import PopoverController from "shadcn-rails-stimulus/controllers/popover_controller"

application.register("shadcn--popover", PopoverController)

Targets

Name Description
trigger The element that triggers the popover
content The popover content element

Values

Name Type Default Description
open Boolean false Whether the popover is currently open
side String "bottom" Side to position the popover (top/right/bottom/left)
align String "center" Alignment relative to trigger (start/center/end)
modal Boolean false Whether to block interaction with the rest of the page

Actions

Action Description
toggle Toggles the popover open/closed
show Opens the popover
hide Closes the popover
close Alias for hide

TypeScript

Type definitions are included. Import types as needed:

import type { PopoverController } from "shadcn-rails-stimulus"

Accessibility

  • Press Escape to close the popover
  • Click outside the popover to close it
  • When modal: true, focus is trapped within the popover
  • Ensure trigger elements are keyboard accessible (buttons, not divs)
  • Keep popover content concise and focused
  • Avoid nesting popovers within popovers