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

Composition

You build a component by nesting helpers, the way shadcn/ui nests React components. The view ends up shaped like the HTML it renders.

Helpers exist only for what is installed

Each component installs its own helper module: button writes app/helpers/ui/button_helper.rb, which defines Ui::ButtonHelper#ui_button. Rails automatically includes everything under app/helpers/, so an installed helper is available in every view with no include.

The helper of a component that is not installed does not exist, and calling it raises NoMethodError when the page renders. Check installed[].helpers before writing the view.

bin/shadwire status --json
bin/shadwire add carousel --yes

Subcomponents are nested helpers

These components don't use ViewComponent slots, and they don't take their content as arguments. You put them together by nesting calls.

<%# Wrong: these props do not exist %>
<%= ui_card(header: "Time", footer: "Save") %>
<%# Right %>
<%= ui_card do %>
  <%= ui_card_header do %>
    <%= ui_card_title { "Team" } %>
    <%= ui_card_description { "Manage who has access." } %>
  <% end %>
  <%= ui_card_content { "Body" } %>
  <%= ui_card_footer { ui_button { "Save" } } %>

bin/shadwire info <name> lists the helper for each part. Look it up rather than guessing: the helper is ui_card_content, not ui_card_body.

Use the whole composition

Don't put everything into one part. The header, title and description each render their own markup, and putting all of it in the content loses that.

<%# Wrong: everything dumped into the content %>
<%= ui_card do %>
  <%= ui_card_content do %>
    <h3>Team</h3>
    <p>Manage who has access.</p>
  <% end %>
<% end %>

Items go inside their group

<%# Wrong: item rendered straight at the root %>
<%= ui_select do %>
  <%= ui_select_item(value: "a") { "A" } %>
<% end %>
<%# Right %>
<%= ui_select(name: "role", placeholder: "Select a role") do %>
  <%= ui_select_trigger { ui_select_value } %>
  <%= ui_select_content do %>
    <%= ui_select_item(value: "admin") { "Admin" } %>
  <% end %>
<% end %>

The same goes for ui_tabs_trigger inside ui_tabs_list, ui_breadcrumb_item inside ui_breadcrumb_list, and ui_pagination_item inside ui_pagination_content.

Overlays always need a title

dialog, alert-dialog, sheet and drawer need their title component so screen readers can announce them, even when the design has no visible title.

<%# Wrong: the dialog has no accessible name %>
<%= ui_dialog_content do %>
  <p>Are you sure?</p>
<% end %>
<%# Right: visually hidden, but announced %>
<%= ui_dialog_content do %>
  <%= ui_dialog_header do %>
    <%= ui_dialog_title(class: "sr-only") { "Confirm deletion" } %>
  <% end %>
  <p>Are you sure?</p>
<% end %>

Interactive components need Stimulus

29 of the 57 components ship a controller. bin/shadwire info <name> tells you which through requiresStimulus, and status.stack says whether the app has importmap and stimulus-rails.

The controllers install into app/javascript/controllers/ and register themselves through controllers/index.js's eagerLoadControllersFrom("controllers", application). If your app registers controllers explicitly, register the new ones there too.

Rendering the class directly

The helper only wraps the class, so these two render the same thing:

<%= ui_button(variant: :outline) { "Save" } %>
<%= render Ui::ButtonComponent.new(variant: :outline) do %>Save<% end %>

In views, use the helper. Use the class when you need the component object itself, or inside another ViewComponent. ViewComponents don't get helpers automatically, so include the module to call ui_button from a component's template:

class DashboardCardComponent < ViewComponent::Base
  # Components do not get helpers automatically.
  include Ui::ButtonHelper
end