Senren UI
Components / Invite Member Dialog

Invite Member Dialog

Stable Stimulus: senren--invite-member-dialog

team invitation flow

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/.../invite_member_dialog_example.html.erb
<div class="w-full max-w-md">
  <%= render Senren::InviteMemberDialogComponent.new(
    title: "Invite teammate",
    description: "Send a workspace invite with the right role.",
    roles: ["Member", "Admin", "Billing"]
  ) do |dialog| %>
    <% dialog.with_trigger do %>
      <%= render(Senren::ButtonComponent.new(variant: :primary)) { "Invite teammate" } %>
    <% 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 invite_member_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 invite_member_dialog --client

Dependencies are resolved by senren:add: dialog, form, input, button.

At a glance #

Category Saas
Class name Senren::InviteMemberDialogComponent
Stimulus senren--invite-member-dialog
Variants default
Depends on dialog, form, input, button
Pairs with team_member_list

Source #

app/components/senren/invite_member_dialog_component.html.erb
<%= tag.div(**root_attrs("inline-block", data: { controller: "senren--invite-member-dialog" })) do %>
  <% if trigger? %>
    <span data-action="click->senren--invite-member-dialog#open" data-senren--invite-member-dialog-target="trigger"><%= trigger %></span>
  <% else %>
    <button type="button" class="inline-flex h-10 cursor-pointer items-center justify-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-4 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] hover:opacity-90" data-action="click->senren--invite-member-dialog#open" data-senren--invite-member-dialog-target="trigger">
      <%= button_label %>
    </button>
  <% end %>

  <div hidden data-senren--invite-member-dialog-target="overlay" class="fixed inset-0 z-40 bg-[hsl(var(--senren-foreground)/0.42)] backdrop-blur-sm"></div>
  <div hidden id="<%= dom_id %>" data-senren--invite-member-dialog-target="panel" role="dialog" aria-modal="true" aria-labelledby="<%= dom_id %>-title" tabindex="-1" class="fixed left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2 rounded-(--senren-radius) border border-[hsl(var(--senren-border))] bg-[hsl(var(--senren-popover))] p-6 text-[hsl(var(--senren-popover-foreground))] shadow-lg">
    <h2 id="<%= dom_id %>-title" class="font-display text-lg font-semibold"><%= title %></h2>
    <% if description.present? %>
      <p class="mt-1.5 text-sm leading-6 text-[hsl(var(--senren-muted-foreground))]"><%= description %></p>
    <% end %>

    <div class="mt-5 space-y-4">
      <% if content? %>
        <%= content %>
      <% else %>
        <label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-email">Email</label>
        <input id="<%= dom_id %>-email" name="<%= email_name %>" type="email" placeholder="teammate@company.com" class="h-10 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-3 text-sm outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
        <label class="block text-sm font-medium text-[hsl(var(--senren-foreground))]" for="<%= dom_id %>-role">Role</label>
        <select id="<%= dom_id %>-role" name="<%= role_name %>" class="h-10 w-full rounded-(--senren-radius) border border-[hsl(var(--senren-input))] bg-[hsl(var(--senren-background))] px-3 text-sm outline-none focus:ring-2 focus:ring-[hsl(var(--senren-ring))]">
          <% roles.each do |role| %><option><%= role %></option><% end %>
        </select>
      <% end %>
    </div>

    <div class="mt-6 flex justify-end gap-2">
      <button type="button" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) px-4 text-sm font-medium hover:bg-[hsl(var(--senren-accent))]" data-action="click->senren--invite-member-dialog#close">Cancel</button>
      <% if footer? %><%= footer %><% else %><button type="submit" class="inline-flex h-10 cursor-pointer items-center rounded-(--senren-radius) bg-[hsl(var(--senren-primary))] px-4 text-sm font-medium text-[hsl(var(--senren-primary-foreground))] hover:opacity-90">Send invite</button><% end %>
    </div>
  </div>
<% end %>
app/components/senren/invite_member_dialog_component.rb
# frozen_string_literal: true

module Senren
  class InviteMemberDialogComponent < BaseComponent
    renders_one :trigger
    renders_one :footer

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

    def initialize(title: 'Invite teammate', description: 'Send an invitation to join this workspace.',
                   email_name: 'email', role_name: 'role', roles: %w[Member
                                                                     Admin], button_label: 'Invite member', id: nil, class_name: nil, **html)
      super(variant: :default, size: :md, class_name: class_name, **html)
      @title = title
      @description = description
      @email_name = email_name
      @role_name = role_name
      @roles = Array(roles)
      @button_label = button_label
      @dom_id = id || "senren-invite-member-#{SecureRandom.hex(3)}"
    end

    attr_reader :title, :description, :email_name, :role_name, :roles, :button_label, :dom_id
  end
end

AI agent rules #

Use for

  • +team invitation flow

Avoid

  • -bulk imports - use a dedicated screen

Accessibility #

  • Inherit dialog accessibility; validate email server-side.