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

Styling

Use variants to change how a component looks and class: to place it on the page. Colours come from tokens, and your classes take priority.

Semantic tokens only

Components take every colour from a shadcn token, so light mode, dark mode and a new theme all work without changes to the component code. Do the same in your own markup.

<%# Wrong: hardcoded colours and a hand-written dark override %>
<%= ui_card(class: "bg-white text-gray-900 dark:bg-gray-900 dark:text-white") do %>
<%# Right: the tokens already cover both modes %>
<%= ui_card do %>

Tokens come in pairs, <surface> and <surface>-foreground: bg-primary text-primary-foreground, bg-muted text-muted-foreground, bg-destructive text-destructive-foreground. The full list is on the Theming page.

Don't use dark: to set colours. Keep the prefix for differences that aren't about colour, such as showing a different image in dark mode.

class: and class_name: are the same thing

Both end up in the same place. Use whichever you prefer; class: is what Rails code usually uses.

<%= ui_button(class: "w-full") { "Save" } %>
<%= ui_button(class_name: "w-full") { "Save" } %>

Your classes take priority

Classes are combined in this order, with yours last:

base_classes → variant_classes → size_classes → your class

When two classes set the same property, like the component's w-full and your w-80, the earlier one is dropped, the same way cn() works in shadcn/ui. Otherwise both would end up on the element and whichever Tailwind happened to emit last would apply. This works for the common utilities: colour, text, size, spacing, radius, shadow, display, position and alignment. For anything else, add ! to the end of your class to force it.

So class: can override the component's own classes. Even so, don't use it to restyle a component:

<%# Wrong: fighting the design system %>
<%= ui_button(class: "bg-red-600 hover:bg-red-700") { "Delete" } %>
<%# Right: the variant already exists %>
<%= ui_button(variant: :destructive) { "Delete" } %>

Use class: for layout: width, margin, where it sits in a grid.

<%= ui_button(variant: :outline, class: "w-full sm:w-auto") { "Save" } %>

HTML attributes are passed through

Any argument the component doesn't recognise goes to the rendered element as an HTML attribute, and Rails' nested hashes like data: { … } work too.

<%= ui_button(id: "save", data: { turbo: false, controller: "form" },
              "aria-describedby": "hint") { "Save" } %>

<%= ui_button(tag: :a, href: post_path(post), data: { turbo_method: :delete }) { "Delete" } %>

Use tag: to change the element, on components that support it. button renders a <button> by default and an <a> with tag: :a.

Prefer variants and sizes over utilities

Check which variants and sizes exist before writing your own classes:

$ bin/shadwire info button
  variants: default | destructive | outline | secondary | ghost | link
  sizes:    default | sm | lg | icon
<%# Wrong %>
<%= ui_button(class: "h-8 px-3 text-xs border") { "Small" } %>
<%# Right %>
<%= ui_button(variant: :outline, size: :sm) { "Small" } %>

Utility conventions

Follow the same conventions as the components, so your code looks like the code the CLI installed:

  • size-9 rather than h-9 w-9, when width and height match.
  • gap-* with flex or grid, rather than space-x-* / space-y-*.
  • truncate rather than overflow-hidden text-ellipsis whitespace-nowrap.
  • no manual z-index on overlays: dialog, sheet, popover and dropdown-menu already handle their own stacking.