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

Accessibility

The components follow the WAI-ARIA APG patterns and use native HTML elements wherever they can. Only a few things are left for you to do.

What comes built in

Area What the component already does
Native element first Dialog, Alert Dialog, Sheet and Drawer use the native <dialog>: focus trapping, Esc and focus restoration come from the browser. Checkbox, Radio Group and Switch are styled native inputs.
Roles and states Tabs exposes tablist/tab/tabpanel; Select is an APG combobox with aria-activedescendant; Toggle uses aria-pressed; Progress exposes aria-valuenow.
Keyboard Arrow keys navigate menus, tabs, select and toggle group; typeahead matches by prefix in menus and select; Esc closes overlays.
Visible focus Every interactive control shows a focus ring in the ring colour when it has :focus-visible.
Decoration is hidden Icons render aria-hidden="true" by default; decorative separators leave the accessibility tree.

In the Shadwire repository, every component has render tests, and an accessibility test goes through the site's pages checking structure, labels and navigation. Browser tests run in headless Chrome on every change.

What's left to you

Naming overlays

<dialog> takes care of focus and the keyboard, but it cannot come up with a name. Every dialog, sheet and drawer needs its title component; add class: "sr-only" when the design has no visible title.

<%= ui_dialog_content do %>
  <%= ui_dialog_header do %>
    <%= ui_dialog_title(class: "sr-only") { "Confirm deletion" } %>
  <% end %>
  <p>This action cannot be undone.</p>
<% end %>

Naming icons that carry meaning

An icon next to text is decoration and should stay hidden. An icon on its own is the label, so pass label: or label the control around it. There is more on the Icons page.

<%= ui_icon("bell", label: "Notifications") %>

Tying label to control

Since there is no form builder, you set for: and id: yourself. ui_field lays out the row, but it cannot guess the control's id. See Forms.

Contrast after retheming

The default colours have enough contrast. If you change them, check yours: change <surface> and <surface>-foreground together, and test both modes.

Heading hierarchy

ui_card_title and ui_dialog_title set how a title looks, not its heading level. If the page needs a heading structure, add the h1–h3 elements yourself.

Testing in your app

The installed components are part of your code, so you test them like any other ViewComponent. Make sure your tests check the accessible name, since it depends on what you pass in:

test "dialog renders a native <dialog> with an accessible name" do
  render_inline(Ui::Dialog::ContentComponent.new) do
    render_inline(Ui::Dialog::TitleComponent.new) { "Edit profile" }
  end

  assert_selector "dialog[aria-labelledby]"
  assert_text "Edit profile"
end

Found a problem?

If a component has an accessibility problem, that is a bug in Shadwire, not in your app. Please open an issue. Meanwhile the file is yours, so you can fix it locally; run bin/shadwire diff before your next update so you don't lose the fix.