Senren UI
Components / Sheet

Sheet

Stable Stimulus: senren--sheet

side panels

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/.../sheet_example.html.erb
<div class="flex w-full justify-center">
  <%= render Senren::SheetComponent.new(variant: :right) do |s| %>
    <% s.with_trigger do %>
      <%= render(Senren::ButtonComponent.new(variant: :secondary)) { "Open sheet" } %>
    <% end %>
    <% s.with_title { "Edit profile" } %>
    <% s.with_description { "Update your account details. Changes save when you close the sheet." } %>
    <% s.with_body do %>
      <div class="space-y-3">
        <%= render(Senren::LabelComponent.new(for_field: "sheet_name")) { "Display name" } %>
        <%= render Senren::InputComponent.new(name: "name", id: "sheet_name", value: "Senren") %>
      </div>
    <% end %>
    <% s.with_footer do %>
      <button type="button" data-action="click->senren--sheet#close" class="cursor-pointer inline-flex h-10 items-center rounded-md px-4 text-sm">Cancel</button>
      <%= render(Senren::ButtonComponent.new(variant: :primary)) { "Save" } %>
    <% 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 sheet --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 sheet --client

Dependencies are resolved by senren:add: button.

At a glance #

Category Overlays
Class name Senren::SheetComponent
Stimulus senren--sheet
Variants right, left, top, bottom
Depends on button
Pairs with form

Source #

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

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

  <div data-senren--sheet-target="panel" hidden role="dialog" aria-modal="true"
       data-open="false"
       class="fixed z-50 bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))] shadow-lg transition-transform duration-200 <%= self.class::VARIANTS[variant] %>">
    <div class="flex h-full flex-col p-6">
      <% if title? %>
        <h2 class="text-lg font-semibold"><%= title %></h2>
      <% end %>
      <% if description? %>
        <p class="text-sm text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
      <% end %>
      <div class="mt-4 flex-1 overflow-auto">
        <%= body || content %>
      </div>
      <% if footer? %>
        <div class="mt-4 border-t border-[hsl(var(--senren-border))] pt-4">
          <%= footer %>
        </div>
      <% end %>
    </div>
    <button type="button" data-action="click->senren--sheet#close" aria-label="Close"
            class="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100">
      <span aria-hidden="true">×</span>
    </button>
  </div>
</div>
app/components/senren/sheet_component.rb
# frozen_string_literal: true

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

    VARIANTS = {
      right: 'right-0 top-0 h-full w-full max-w-md translate-x-full data-[open=true]:translate-x-0',
      left: 'left-0  top-0 h-full w-full max-w-md -translate-x-full data-[open=true]:translate-x-0',
      top: 'left-0 top-0 w-full h-1/2 -translate-y-full data-[open=true]:translate-y-0',
      bottom: 'left-0 bottom-0 w-full h-1/2 translate-y-full data-[open=true]:translate-y-0'
    }.freeze

    SIZES = { md: '' }.freeze

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

    attr_reader :dom_id
  end
end

AI agent rules #

Use for

  • +side panels
  • +mobile filters
  • +edit panels

Avoid

  • -destructive confirmation - use alert_dialog

Accessibility #

  • Trap focus when open.
  • Escape to close.
  • aria-modal=true while open.