Button
Stableprimary action
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/.../button_example.html.erb
<div class="flex flex-wrap items-center justify-center gap-2">
<%= render(Senren::ButtonComponent.new(variant: :primary)) { "Primary" } %>
<%= render(Senren::ButtonComponent.new(variant: :secondary)) { "Secondary" } %>
<%= render(Senren::ButtonComponent.new(variant: :destructive)) { "Destructive" } %>
<%= render(Senren::ButtonComponent.new(variant: :ghost)) { "Ghost" } %>
<%= render(Senren::ButtonComponent.new(variant: :link)) { "Link" } %>
</div>
<div class="mt-3 flex flex-wrap items-center justify-center gap-2">
<%= render(Senren::ButtonComponent.new(variant: :primary, size: :sm)) { "Small" } %>
<%= render(Senren::ButtonComponent.new(variant: :primary, size: :md)) { "Medium" } %>
<%= render(Senren::ButtonComponent.new(variant: :primary, size: :lg)) { "Large" } %>
</div>
Install this component #
Copy the official component into your app
Use this when you want the Senren-maintained implementation copied into app/components/senren.
Terminal
bin/rails senren:add button
Create a custom component with the same conventions
Use this when you need an app-specific static component that follows Senren's ViewComponent structure.
Terminal
bin/rails generate senren:component button --no-client
At a glance #
Category
Actions
Class name
Senren::ButtonComponent
Stimulus
—
Variants
default, primary, secondary, destructive, ghost, link
Depends on
—
Pairs with
form, dialog, dropdown_menu, alert_dialog
Source #
app/components/senren/button_component.html.erb
<% base = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-(--senren-radius) font-medium transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--senren-ring))] disabled:opacity-50 disabled:pointer-events-none disabled:cursor-not-allowed" %>
<% if as == :a %>
<%= tag.a content, href: safe_url(href), **root_attrs(base) %>
<% else %>
<%= tag.button content, **root_attrs(base, type: type) %>
<% end %>
app/components/senren/button_component.rb
# frozen_string_literal: true
module Senren
class ButtonComponent < BaseComponent
VARIANTS = {
default: 'bg-[hsl(var(--senren-secondary))] text-[hsl(var(--senren-secondary-foreground))] hover:opacity-90',
primary: 'bg-[hsl(var(--senren-primary))] text-[hsl(var(--senren-primary-foreground))] hover:opacity-90',
secondary: 'bg-[hsl(var(--senren-secondary))] text-[hsl(var(--senren-secondary-foreground))] hover:opacity-90',
destructive: 'bg-[hsl(var(--senren-destructive))] text-[hsl(var(--senren-destructive-foreground))] hover:opacity-90',
ghost: 'bg-transparent text-[hsl(var(--senren-foreground))] hover:bg-[hsl(var(--senren-accent))]',
link: 'bg-transparent text-[hsl(var(--senren-primary))] underline-offset-4 hover:underline'
}.freeze
SIZES = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base'
}.freeze
# FOR AI AGENTS AND HUMANS -- how `type` behaves, and why it changed.
#
# `type:` defaults to nil, so the attribute is omitted and the browser's own
# rule applies: a <button> inside a <form> submits it, one outside a form
# does nothing. Write what you would write in plain HTML; it behaves the
# same.
#
# <%= form_with url: sessions_path do %>
# <%= render(Senren::ButtonComponent.new(variant: :primary)) { "Sign in" } %>
# <% end %> # submits, as HTML says
#
# <%= render(Senren::ButtonComponent.new(type: :button)) { "Open" } %>
# # a trigger: say so
#
# This used to default to "button", which silently overrode the platform. A
# button rendered inside a form did nothing when clicked -- no submit, no
# navigation, nothing in the console -- and that is what users reported:
# "I click submit and it does not redirect". Every auth example this library
# ships had the bug, so anyone copying a snippet inherited it.
#
# The trade is deliberate. The old default failed silently; this one fails
# loudly, by submitting a form you did not mean to submit. A visible wrong
# is cheaper to find than an invisible nothing.
#
# Agents: writing a form? Do not pass `type:` at all. Writing a trigger for
# a dialog, sheet, or menu, which must not submit? Pass `type: :button`.
def initialize(variant: :default, size: :md, type: nil, as: :button, href: nil, class_name: nil, **html)
super(variant: variant, size: size, class_name: class_name, **html)
@type = type
@as = href ? :a : as
@href = href
end
attr_reader :type, :as, :href
end
end
AI agent rules #
Use for
- +primary action
- +secondary action
- +form submit
- +destructive action
Avoid
- -using destructive variant for non-destructive actions
Accessibility #
- Icon-only buttons must include aria-label.
- Use button for actions and anchor for navigation.