Shadwire

Search the documentation

Search the documentation and component pages by title and by content.

Get started
Introduction
Installation
shadwire.json
Theming
Dark mode
Tools
CLI
Agent skill
Registry
llms.txt
Guides
Composition
Styling
Forms
Icons
Accessibility
Localisation
Blocks
Overview
Components
Overview
Accordion
Alert
Alert Dialog
Aspect Ratio
Avatar
Badge
Breadcrumb
Button
Button Group
Calendar
Card
Carousel
Chart
Checkbox
Collapsible
Combobox
Command
Context Menu
Data Table
Date Picker
Dialog
Drawer
Dropdown Menu
Empty
Field
Hover Card
Icon
Input
Input Group
Input OTP
Item
Kbd
Label
Menubar
Native Select
Navigation Menu
Pagination
Popover
Progress
Radio Group
Resizable
Scroll Area
Select
Separator
Sheet
Sidebar
Skeleton
Slider
Sonner
Spinner
Switch
Table
Tabs
Textarea
Toggle
Toggle Group
Tooltip

Forms

Lay out each form row with field, and connect the controls to form_with yourself. There is no form builder.

Wrap every control in ui_field

Don't build form rows out of divs and spacing utilities. field already takes care of the label, the description, the error message and the spacing between them.

<%# Wrong %>
<div class="space-y-2">
  <%= ui_label(for: "email") { "Email" } %>
  <%= ui_input(type: :email, id: "email", name: "email") %>
  <p class="text-sm text-muted-foreground">We never share it.</p>
</div>
<%# Right %>
<%= ui_field do %>
  <%= ui_field_label(for: "email") { "Email" } %>
  <%= ui_input(type: :email, id: "email", name: "email") %>
  <%= ui_field_description { "We never share it." } %>
<% end %>

All the field parts:

Helper Role
ui_field The row. Takes orientation: :vertical (default), :horizontal or :responsive.
ui_field_group Stacks several fields with consistent spacing.
ui_field_set + ui_field_legend Groups related checkboxes or radios, instead of a div with a heading.
ui_field_label The field's label.
ui_field_title / ui_field_description Supporting text above and below the control.
ui_field_error The error message, with role="alert".
ui_field_content Wraps the control when it needs a container of its own.
ui_field_separator Divides sections inside a group.

Validation state

Pass invalid: true to ui_field. It sets data-invalid and turns the row's text red (the destructive colour). The message goes in ui_field_error.

<%= ui_field(invalid: user.errors[:email].any?) do %>
  <%= ui_field_label(for: "email") { "Email" } %>
  <%= ui_input(type: :email, id: "email", name: "email",
               "aria-invalid": user.errors[:email].any?) %>
  <%= ui_field_error(errors: user.errors[:email]) %>
<% end %>

ui_field_error takes either block content or an array in errors:, and renders nothing when both are empty, so you can always leave it in the template without an if.

There is no form builder integration

There is no f.ui_input. The controls pass any HTML attribute through to the element, so you connect them to form_with yourself:

<%= form_with(model: @user) do |form| %>
  <%= ui_field(invalid: @user.errors[:email].any?) do %>
    <%= ui_field_label(for: form.field_id(:email)) { "Email" } %>
    <%= ui_input(type: :email,
                 id: form.field_id(:email),
                 name: form.field_name(:email),
                 value: @user.email) %>
    <%= ui_field_error(errors: @user.errors[:email]) %>

form.field_name and form.field_id produce the user[email] and user_email names Rails expects, so the params arrive as usual. The same works for ui_checkbox, ui_switch, ui_textarea, ui_select, ui_radio_group and ui_slider.

<%= hidden_field_tag form.field_name(:admin), "0", id: nil %>
<%= ui_checkbox(name: form.field_name(:admin), id: form.field_id(:admin),
                value: "1", checked: @user.admin?) %>

Choosing the control

Situation Control
One line of text ui_input
Several lines ui_textarea
Text with a prefix, suffix or button ui_input_group
One of a long list ui_select, or combobox if people need to search it
One of a few visible options ui_radio_group
The native picker on mobile, no JS ui_native_select
Several independent choices ui_checkbox
Two to seven compact choices ui_toggle_group
A setting applied immediately ui_switch
An approximate number ui_slider
An exact number ui_input(type: :number)
A date or a range date-picker, or ui_calendar inline
A one-time code ui_input_otp

The date picker is a recipe

There is no Ui::DatePickerComponent. A date field is a ui_popover with a ui_calendar inside, and the ui-date-picker controller shows the chosen date on the trigger and closes the popover. The calendar holds the value: give it name: and it renders a hidden input, so the date is submitted like any other field.

<div data-controller="ui-date-picker" data-ui-date-picker-format-value="long">
  <%= ui_popover do %>
    <%= ui_popover_trigger(variant: :outline, class: "w-[212px] justify-between font-normal") do %>
      <span data-ui-date-picker-target="label" data-empty="true"
            class="data-[empty=true]:text-muted-foreground">Pick a date</span>
      <%= ui_icon("chevron-down", class: "opacity-50") %>
    <% end %>
    <%= ui_popover_content(align: :start, class: "w-auto! p-0!") do %>

The popover needs w-auto! p-0!, because its default w-72 and padding leave too little room for the calendar. The variations are calendar arguments: mode: :range, caption_layout: :dropdown, dir: :rtl. bin/shadwire info date-picker lists them all, and the Date Picker page shows each one live.

Input group has its own input

Inside ui_input_group, use the group's own helpers instead of a plain ui_input.

<%# Wrong %>
<%= ui_input_group do %>
  <%= ui_input_group_addon { ui_icon("search") } %>
  <%= ui_input(name: "q") %>
<% end %>
<%# Right %>
<%= ui_input_group do %>
  <%= ui_input_group_addon { ui_icon("search") } %>
  <%= ui_input_group_input(name: "q", placeholder: "Search") %>
<% end %>