Senren UI
Components / Input

Input

Stable

text

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/.../input_example.html.erb
<div class="w-full max-w-sm space-y-3 text-left">
  <%= render Senren::InputComponent.new(name: "demo_input_a", placeholder: "you@company.com") %>
  <%= render Senren::InputComponent.new(name: "demo_input_b", placeholder: "Disabled", disabled: true) %>
  <%= render Senren::InputComponent.new(name: "demo_input_c", value: "Invalid value", variant: :error) %>
</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 input

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 input --no-client

Dependencies are resolved by senren:add: label.

At a glance #

Category Forms
Class name Senren::InputComponent
Stimulus
Variants default, error
Depends on label
Pairs with form, label, alert

Source #

app/components/senren/input_component.html.erb
<%= tag.input(**root_attrs(input_classes, id: id, name: name, type: type, value: value, placeholder: placeholder, "aria-invalid": variant == :error)) %>
app/components/senren/input_component.rb
# frozen_string_literal: true

module Senren
  class InputComponent < BaseComponent
    VARIANTS = {
      default: 'border-[hsl(var(--senren-input))] focus-visible:ring-[hsl(var(--senren-ring))]',
      error: 'border-[hsl(var(--senren-destructive))] focus-visible:ring-[hsl(var(--senren-destructive))]'
    }.freeze

    SIZES = {
      sm: 'h-8  text-sm  px-2.5',
      md: 'h-10 text-sm  px-3',
      lg: 'h-12 text-base px-4'
    }.freeze

    # Base classes shared by all input types.
    # NOTE: `flex` is intentionally omitted — it breaks browser-native
    #       date/datetime-local/time picker UI on some engines.
    BASE_CLASSES = 'w-full rounded-(--senren-radius) border bg-[hsl(var(--senren-background))] text-[hsl(var(--senren-foreground))] placeholder:text-[hsl(var(--senren-muted-foreground))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50'

    # File-type inputs get a styled button instead of unstyled browser defaults.
    FILE_CLASSES = 'file:mr-3 file:h-full file:cursor-pointer file:border-0 file:border-r file:border-solid file:border-[hsl(var(--senren-border))] file:bg-[hsl(var(--senren-muted))] file:px-3 file:text-sm file:font-medium file:text-[hsl(var(--senren-foreground))]'

    # Non-file inputs get standard font styling for the placeholder.
    TEXT_FILE_CLASSES = 'file:border-0 file:bg-transparent file:text-sm file:font-medium'

    def initialize(name:, type: 'text', value: nil, placeholder: nil, id: nil, variant: :default, size: :md,
                   class_name: nil, **html)
      super(variant: variant, size: size, class_name: class_name, **html)
      @name = name
      @type = type
      @value = value
      @placeholder = placeholder
      @id = id || name.to_s.parameterize
    end

    attr_reader :name, :type, :value, :placeholder, :id

    def input_classes
      file_type? ? "#{BASE_CLASSES} #{FILE_CLASSES}" : "#{BASE_CLASSES} #{TEXT_FILE_CLASSES}"
    end

    def file_type?
      type.to_s == 'file'
    end
  end
end

AI agent rules #

Use for

  • +text
  • +email
  • +password
  • +number
  • +search
  • +url

Avoid

  • -storing structured data in a single text input

Accessibility #

  • Pair with label; use aria-invalid and aria-describedby on errors.