Field
Composable form field wrappers for labels, descriptions, errors, and layout.
Quick Start
<%= kui(:field) do %>
<%= kui(:field, :label, for: :email) { "Email address" } %>
<%= kui(:input, type: :email, id: :email, name: :email) %>
<%= kui(:field, :description) { "We'll never share your email." } %>
<%= kui(:field, :error) { @user.errors[:email].first } %>
<% end %>
Locals
Field
| Local | Type | Default |
|---|---|---|
orientation: |
:vertical | :horizontal | :responsive |
:vertical |
invalid: |
Boolean | false |
disabled: |
Boolean | false |
css_classes: |
String | "" |
**component_options |
Hash | {} |
FieldGroup
| Local | Type | Default |
|---|---|---|
css_classes: |
String | "" |
**component_options |
Hash | {} |
FieldSet
| Local | Type | Default |
|---|---|---|
css_classes: |
String | "" |
**component_options |
Hash | {} |
FieldLegend
| Local | Type | Default |
|---|---|---|
variant: |
:legend | :label |
:legend |
css_classes: |
String | "" |
**component_options |
Hash | {} |
FieldError
| Local | Type | Default |
|---|---|---|
errors: |
Array | [] |
css_classes: |
String | "" |
**component_options |
Hash | {} |
Sub-parts
| Part | Usage | Element | Purpose |
|---|---|---|---|
:label |
kui(:field, :label) |
<label> |
Accessible label (wraps Label component) |
:content |
kui(:field, :content) |
<div> |
Groups label + description in a flex column |
:title |
kui(:field, :title) |
<div> |
Lightweight title (alternative to label) |
:description |
kui(:field, :description) |
<p> |
Helper text below field |
:error |
kui(:field, :error) |
<div role="alert"> |
Error message (only renders when content present) |
:separator |
kui(:field, :separator) |
<div> |
Visual divider with optional text |
FieldSet sub-parts:
| Part | Usage | Element | Purpose |
|---|---|---|---|
:legend |
kui(:field_set, :legend) |
<legend> |
Semantic legend with variant styling |
All sub-parts accept css_classes: and **component_options.
Anatomy
FieldGroup
├── Field (vertical)
│ ├── Field Label
│ ├── <input>
│ ├── Field Description
│ └── Field Error
├── Field Separator
└── Field (horizontal)
├── <checkbox>
└── Field Content
├── Field Label
└── Field Description
FieldSet
├── FieldSet Legend
├── Field Description
└── Field (horizontal) × N
Usage
Vertical (default)
The standard layout — label above input, full width.
<%= kui(:field) do %>
<%= kui(:field, :label, for: :name) { "Full name" } %>
<%= kui(:input, id: :name, name: :name) %>
<%= kui(:field, :description) { "As it appears on your ID." } %>
<% end %>
Horizontal
Label and control side by side — ideal for checkboxes, radios, and switches.
<%= kui(:field, orientation: :horizontal) do %>
<%= kui(:checkbox, id: :dark_mode, name: :dark_mode) %>
<%= kui(:field, :label, for: :dark_mode) { "Enable dark mode" } %>
<% end %>
With description using FieldContent:
<%= kui(:field, orientation: :horizontal) do %>
<%= kui(:checkbox, id: :newsletter, name: :newsletter) %>
<%= kui(:field, :content) do %>
<%= kui(:field, :label, for: :newsletter) { "Newsletter" } %>
<%= kui(:field, :description) { "Receive weekly updates." } %>
<% end %>
<% end %>
Responsive
Stacks vertically on mobile, switches to horizontal at the @md container
query breakpoint. Requires a parent FieldGroup for the container query scope.
<%= kui(:field_group) do %>
<%= kui(:field, orientation: :responsive) do %>
<%= kui(:field, :label, for: :company) { "Company" } %>
<%= kui(:input, id: :company, name: :company) %>
<% end %>
<% end %>
Validation Errors
Set invalid: true on the field to apply error styling. Use FieldError to
display messages — it only renders when content is present.
<%= kui(:field, invalid: true) do %>
<%= kui(:field, :label, for: :email) { "Email" } %>
<%= kui(:input, id: :email, name: :email) %>
<%= kui(:field, :error) { "Please enter a valid email." } %>
<% end %>
Pass Rails model errors directly via the errors: prop. Single errors render
as text; multiple errors render as a bulleted list:
<%= kui(:field, :error, errors: @user.errors[:email]) %>
FieldGroup
Stacks multiple fields with consistent gap-7 spacing.
<%= kui(:field_group) do %>
<%= kui(:field) do %>
<%= kui(:field, :label, for: :first_name) { "First name" } %>
<%= kui(:input, id: :first_name, name: :first_name) %>
<% end %>
<%= kui(:field) do %>
<%= kui(:field, :label, for: :last_name) { "Last name" } %>
<%= kui(:input, id: :last_name, name: :last_name) %>
<% end %>
<% end %>
FieldSet
Semantic <fieldset> for grouping related controls (checkboxes, radios).
<%= kui(:field_set) do %>
<%= kui(:field_set, :legend) { "Notifications" } %>
<%= kui(:field, orientation: :horizontal) do %>
<%= kui(:checkbox, id: :email_notifs, name: "notifs[]", value: "email") %>
<%= kui(:field, :label, for: :email_notifs) { "Email" } %>
<% end %>
<%= kui(:field, orientation: :horizontal) do %>
<%= kui(:checkbox, id: :sms_notifs, name: "notifs[]", value: "sms") %>
<%= kui(:field, :label, for: :sms_notifs) { "SMS" } %>
<% end %>
<% end %>
FieldSeparator
Visual divider between fields, with optional centered text.
<%= kui(:field, :separator) { "Or continue with" } %>
Theme
# Field — orientation variants
Kiso::Themes::Field = ClassVariants.build(
base: "group/field flex w-full gap-3 text-foreground data-[invalid=true]:text-error",
variants: {
orientation: {
vertical: "flex-col [&>*]:w-full [&>.sr-only]:w-auto",
horizontal: "flex-row items-center ...",
responsive: "flex-col ... @md/field-group:flex-row ..."
}
},
defaults: { orientation: :vertical }
)
# FieldGroup — container query scope
Kiso::Themes::FieldGroup = ClassVariants.build(
base: "group/field-group @container/field-group flex w-full flex-col gap-7 ..."
)
# FieldSet + FieldLegend
Kiso::Themes::FieldSet = ClassVariants.build(
base: "flex flex-col gap-6 ..."
)
Kiso::Themes::FieldLegend = ClassVariants.build(
base: "mb-3 font-medium",
variants: { variant: { legend: "text-base", label: "text-sm" } },
defaults: { variant: :legend }
)
Accessibility
- Field renders
<div role="group">withdata-orientationattribute - FieldLabel renders
<label>with properforattribute linking - FieldError renders
<div role="alert">for screen reader announcements - FieldSet renders semantic
<fieldset>with<legend> invalid: truesetsdata-invalid="true"which cascades error colordisabled: truesetsdata-disabled="true"which dims labels via group styling