Input
A styled text input with focus ring and disabled state.
Installation
bin/rails g wabi:add inputExample
render Components::UI::Input.new(name: "email", placeholder: "you@example.com", type: "email")
Invalid state
Pass invalid: true to render aria-invalid="true", which assistive tech announces and styles can target via aria-[invalid]:.
render Components::UI::Input.new(name: "email", value: "not-an-email", type: "email", invalid: true, aria_label: "Email")
Search
No separate component needed — a search field is Input type="search" with a leading icon. type="search" also gives you the browser's native clear (×) button while typing — no JavaScript.
div(class: "relative w-full max-w-sm") do
# decorative magnifying glass — doesn't capture clicks
raw safe(%(<svg class="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground pointer-events-none" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>))
render Components::UI::Input.new(type: "search", placeholder: "Search components…", aria_label: "Search", class: "pl-9")
end
Need search-as-you-type with a suggestions dropdown? Use the async Combobox instead — it wires the debounced fetch and result list for you.
Source
app/components/ui/input.rb
# frozen_string_literal: true
module Components
module UI
# Accessibility: an <input> needs an accessible name. This primitive forwards
# all attrs, so callers MUST supply one of: an associated <label for=> (pass a
# matching `id:`), a wrapping <label>, or `aria_label:` / `aria-label:` directly.
# A bare Input with none of these is an unlabelled control (WCAG 4.1.2 fail).
class Input < Wabi::Base
variants do
base "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 " \
"text-sm ring-offset-background file:border-0 file:bg-transparent " \
"file:text-sm file:font-medium placeholder:text-muted-foreground " \
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring " \
"focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 " \
"aria-[invalid=true]:border-destructive aria-[invalid=true]:focus-visible:ring-destructive"
end
def initialize(type: "text", invalid: false, **attrs)
@type = type
@invalid = invalid
@attrs = attrs
end
def view_template
user_class = @attrs.delete(:class)
input(
type: @type,
aria_invalid: (@invalid ? "true" : nil),
**@attrs,
class: merge_class(tokens, user_class)
)
end
end
end
end
Accessibility
- native <input> element — full browser keyboard support out of the box.
- focus-visible:ring keeps focus state visible without click pollution.
- disabled state via disabled:opacity-50 + disabled:cursor-not-allowed.
- inherits type-specific affordances (email keyboard on mobile, etc.).