Docs
Components
Dialog

Dialog

A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.

<%= dialog do %>
  <%= dialog_trigger do %>
    <%= button() do %>
      Edit Profile
    <% end %>
  <% end %>
  <%= dialog_content class_name: "sm:max-w-[425px]" do %>
    <%= dialog_header do %>
      <%= dialog_title do %>
        Edit profile
      <% end %>
      <%= dialog_description do %>
        Make changes to your profile here. Click save when you're done.
      <% end %>
    <% end %>
    <div class="grid gap-4 py-4">
      <div class="grid grid-cols-4 items-center gap-4">
        <%= label(for: "name", class_name: "text-right") do %>
          Name
        <% end %>
        <%= input(id: "name", value: "Dark Sea", class_name: "col-span-3") %>
      </div>
      <div class="grid grid-cols-4 items-center gap-4">
        <%= label(for: "username", class_name: "text-right") do %>
          Username
        <% end %>
        <%= input(id: "username", value: "@darkseadev", class_name: "col-span-3") %>
      </div>
    </div>
    <%= dialog_footer do %>
      <%= button(type: "submit") do %>
        Save changes
      <% end %>
    <% end %>
  <% end %>
<% end %>

Installation

1

Copy and paste the following code into your project.

Create a new file in app/components/ui/dialog_component.rb and paste the following code:
# frozen_string_literal: true

module Ui
  # Main dialog component that wraps the entire dialog structure
  class DialogComponent < ViewComponent::Base
    def initialize(class_name: nil, **options)
      super
      @class_name = class_name
      @options = options
    end

    def call
      content_tag(:div, class: dialog_classes, data: { controller: 'dialog' }, **@options) do
        content
      end
    end

    private

    def dialog_classes
      "relative #{@class_name}"
    end
  end

  # Component for the semi-transparent backdrop behind the dialog
  class DialogBackdropComponent < ViewComponent::Base
    def call
      content_tag(:div, '', class: backdrop_classes,
                            data: { dialog_target: 'backdrop', action: 'click->dialog#close', state: 'closed' })
    end

    private

    def backdrop_classes
      'fixed inset-0 z-[2000] bg-black/50 transition-opacity duration-200 ease-in-out opacity-0 data-[state=open]:opacity-100'
    end
  end

  # Component for the element that triggers the dialog to open
  class DialogTriggerComponent < ViewComponent::Base
    def initialize(class_name: nil, **options)
      super
      @class_name = class_name
      @options = options
    end

    def call
      if content.respond_to?(:to_s) && !content.to_s.include?('<')
        tag.button(content, class: trigger_classes, data: { action: 'click->dialog#toggle' }, **@options)
      else
        tag.div(class: trigger_classes, data: { action: 'click->dialog#toggle' }, **@options) do
          content
        end
      end
    end

    private

    def trigger_classes
      "inline-flex items-center justify-center #{@class_name}"
    end
  end

  # Component for the main content area of the dialog
  class DialogContentComponent < ViewComponent::Base
    def initialize(class_name: nil, **options)
      super
      @class_name = class_name
      @options = options
    end

    def call
      tag.div(class: content_classes,
              role: 'dialog',
              aria: { modal: true },
              data: {
                dialog_target: 'content',
                state: 'closed'
              },
              **@options) do
        concat(close_button)
        concat(content)
      end
    end

    private

    def content_classes
      "fixed left-[50%] top-[50%] z-[3000] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 transition-all sm:rounded-lg sm:max-w-[425px] #{@class_name} #{animation_classes}"
    end

    def animation_classes
      'opacity-0 scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100'
    end

    def close_button
      tag.button(
        class: 'absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground',
        type: 'button',
        aria: { label: 'Close' },
        data: { action: 'click->dialog#close' }
      ) do
        tag.svg(
          xmlns: 'http://www.w3.org/2000/svg',
          width: '24',
          height: '24',
          viewBox: '0 0 24 24',
          fill: 'none',
          stroke: 'currentColor',
          stroke_width: '2',
          stroke_linecap: 'round',
          stroke_linejoin: 'round'
        ) do
          concat(tag.line(x1: '18', y1: '6', x2: '6', y2: '18'))
          concat(tag.line(x1: '6', y1: '6', x2: '18', y2: '18'))
        end
      end
    end
  end

  # Component for the header section of the dialog content
  class DialogHeaderComponent < ViewComponent::Base
    def initialize(class_name: nil, **options)
      super
      @class_name = class_name
      @options = options
    end

    def call
      tag.div(class: header_classes, **@options) do
        content
      end
    end

    private

    def header_classes
      "flex flex-col space-y-1.5 text-center sm:text-left #{@class_name}"
    end
  end

  # Component for the title of the dialog
  class DialogTitleComponent < ViewComponent::Base
    def initialize(class_name: nil, **options)
      super
      @class_name = class_name
      @options = options
    end

    def call
      tag.h3(class: title_classes, **@options) do
        content
      end
    end

    private

    def title_classes
      "text-lg font-semibold leading-none tracking-tight #{@class_name}"
    end
  end

  # Component for the description text in the dialog
  class DialogDescriptionComponent < ViewComponent::Base
    def initialize(class_name: nil, **options)
      super
      @class_name = class_name
      @options = options
    end

    def call
      tag.p(class: description_classes, **@options) do
        content
      end
    end

    private

    def description_classes
      "text-sm text-muted-foreground #{@class_name}"
    end
  end

  # Component for the footer section of the dialog content
  class DialogFooterComponent < ViewComponent::Base
    def initialize(class_name: nil, **options)
      super
      @class_name = class_name
      @options = options
    end

    def call
      tag.div(class: footer_classes, **@options) do
        content
      end
    end

    private

    def footer_classes
      "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 #{@class_name}"
    end
  end
end
Coming soon.

Usage

<%= dialog do %>
  <%= dialog_trigger do %>
    <%= button() do %>
      Dialog trigger
    <% end %>
  <% end %>
  <%= dialog_content class_name: "sm:max-w-[425px]" do %>
    <%= dialog_header do %>
      <%= dialog_title do %>
        Dialog title
      <% end %>
      <%= dialog_description do %>
        Dialog description
      <% end %>
    <% end %>
    <div class="grid gap-4 py-4">
      Dialog content
    </div>
    <%= dialog_footer do %>
      <%= button(type: "submit") do %>
        Dialog footer
      <% end %>
    <% end %>
  <% end %>
<% end %>