Senren UI
Components / Dialog

Dialog

Stable Stimulus: senren--dialog

confirmation modal

Usage example #

Copy this ERB into a Rails view after installing the component. The snippet below is the same code used by the live preview above.

app/views/.../dialog_example.html.erb
<div class="flex w-full justify-center">
  <%= render Senren::DialogComponent.new do |d| %>
    <% d.with_trigger do %>
      <%= render(Senren::ButtonComponent.new(variant: :primary)) { "Open dialog" } %>
    <% end %>
    <% d.with_title { "Confirm action" } %>
    <% d.with_description { "Are you sure you want to continue? This cannot be undone." } %>
    <% d.with_body do %>
      <p class="text-sm text-[hsl(var(--senren-muted-foreground))]">
        The dialog uses Stimulus for open/close, focus management, and Escape to close. Everything else is server-rendered.
      </p>
    <% end %>
    <% d.with_footer do %>
      <button type="button" data-action="click->senren--dialog#close" class="cursor-pointer inline-flex h-10 items-center rounded-md px-4 text-sm font-medium text-[hsl(var(--senren-foreground))] hover:bg-[hsl(var(--senren-accent))]">Cancel</button>
      <%= render(Senren::ButtonComponent.new(variant: :primary)) { "Continue" } %>
    <% end %>
  <% end %>
</div>

Install this component #

Copy the official component into your app

This component requires Stimulus. Keep --client so the controller is copied with the ViewComponent.

Terminal
bin/rails senren:add dialog --client

Create a custom component with the same conventions

Use this when you need an app-specific component that follows Senren's ViewComponent and Stimulus structure. --client is required for this behavior.

Terminal
bin/rails generate senren:component dialog --client

Dependencies are resolved by senren:add: button.

At a glance #

Category Overlays
Class name Senren::DialogComponent
Stimulus senren--dialog
Variants default
Depends on button
Pairs with form, alert_dialog

Source #

app/components/senren/dialog_component.html.erb
<div data-controller="senren--dialog" data-senren-component="dialog" data-senren--dialog-open-value="<%= open %>" id="<%= dom_id %>">
  <% if trigger? %>
    <span data-action="click->senren--dialog#open" data-senren--dialog-target="trigger" aria-controls="<%= dom_id %>-panel">
      <%= trigger %>
    </span>
  <% else %>
    <button type="button" data-action="click->senren--dialog#open"
            class="hidden" data-senren--dialog-target="trigger" aria-controls="<%= dom_id %>-panel">Open</button>
  <% end %>

  <div data-senren--dialog-target="overlay" hidden
       class="fixed inset-0 z-40 bg-black/50 backdrop-blur-sm"></div>

  <div data-senren--dialog-target="panel" hidden
       role="dialog" aria-modal="true" aria-labelledby="<%= dom_id %>-title"
       id="<%= dom_id %>-panel"
       tabindex="-1"
       class="fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] text-[hsl(var(--senren-popover-foreground))] p-6 shadow-lg focus:outline-none">
    <% if title? %>
      <h2 id="<%= dom_id %>-title" class="text-lg font-semibold leading-none tracking-tight"><%= title %></h2>
    <% end %>
    <% if description? %>
      <p class="mt-1.5 text-sm text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
    <% end %>
    <% if body? %>
      <div class="mt-4"><%= body %></div>
    <% end %>
    <% if footer? %>
      <div class="mt-6 flex justify-end gap-2"><%= footer %></div>
    <% end %>

    <button type="button" data-action="click->senren--dialog#close"
            aria-label="Close"
            class="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
      <span aria-hidden="true">×</span>
    </button>
  </div>
</div>
app/components/senren/dialog_component.rb
# frozen_string_literal: true

module Senren
  class DialogComponent < BaseComponent
    renders_one :trigger
    renders_one :title
    renders_one :description
    renders_one :body
    renders_one :footer

    VARIANTS = { default: '' }.freeze
    SIZES    = { md: '' }.freeze

    def initialize(open: false, id: nil, class_name: nil, **html)
      super(variant: :default, size: :md, class_name: class_name, **html)
      @open = open
      @dom_id = id || "senren-dialog-#{SecureRandom.hex(3)}"
    end

    attr_reader :open, :dom_id
  end
end

AI agent rules #

Use for

  • +confirmation modal
  • +short form modal

Avoid

  • -long multi-step workflows
  • -full page replacement

Accessibility #

  • Must have a visible title.
  • Must support Escape key close.
  • Must trap focus and restore on close.