layered-ui-rails
An open source, Rails 8+ engine that provides WCAG 2.2 AA compliant design tokens, Tailwind CSS utilities, and Stimulus controllers for theme switching, mobile navigation, slide-out panels, modals, and tabs. Ships as pure frontend with no server-side dependencies beyond Rails and Tailwind CSS. See the live demo.
|
|
|
| Light theme (desktop and mobile) | Dark theme (desktop and mobile) |
Getting started
Add to your Gemfile and install:
bundle add layered-ui-rails
bin/rails generate layered:ui:installThe install generator will:
- Copy
layered_ui.csstoapp/assets/tailwind/ - Add
@import "./layered_ui";to yourapplication.css - Add
import "layered_ui"to yourapplication.js
Then update your application layout to render the engine layout:
<%= render template: "layouts/layered_ui/application" %>Requirements
- Ruby on Rails >= 8.0
- Tailwind CSS Rails >= 4.0
- Importmap Rails >= 2.0
- Stimulus Rails >= 1.0
Features
- Dark/light theme - system preference detection with localStorage persistence and manual toggle
- Responsive layout - header, sidebar navigation, main content area, and optional resizable panel
- WCAG 2.2 AA compliant - skip links, focus indicators, ARIA attributes, and 4.5:1 contrast ratios
- Components - buttons, forms, surfaces, tables, tabs, notices, badges, conversations, modals, and pagination
- Optional integrations - Devise authentication and Pagy pagination with styled views
- Customisable branding - Override the default logos and icons and colors
-
Google Lighthouse -
layered-ui-railsscores a perfect 100 across all four Google Lighthouse categories - performance, accessibility, best practices, and SEO
Customising theme tokens
All colors are CSS custom properties on :root. Override any token in your stylesheet (after importing the engine CSS):
/* app/assets/tailwind/application.css */
@import "./layered_ui";
:root {
--accent: 220 80% 55%;
--accent-foreground: 0 0% 100%;
}
.dark {
--accent: 220 80% 65%;
--accent-foreground: 0 0% 9%;
}For dynamic theming (e.g. per-tenant branding), use content_for :l_ui_head to inject content into the layout <head>:
<% content_for :l_ui_head do %>
<style>
:root { --accent: <%= @tenant.accent_hsl %>; --accent-foreground: 0 0% 100%; }
</style>
<% end %>Security: never interpolate user-supplied strings directly into a
<style>tag - this allows CSS injection (Important: Validate or sanitise any user-derived values before interpolation).
CSP compatibility: inline
<style>blocks are blocked by a strictContent-Security-Policy: style-src 'self'header. If your app enforces a strict CSP, add a nonce to the style tag using Rails'content_security_policy_noncehelper - Rails automatically includes the matching nonce in the CSP header:<% content_for :l_ui_head do %> <style nonce="<%= content_security_policy_nonce %>"> :root { --accent: <%= @tenant.accent_hsl %>; --accent-foreground: 0 0% 100%; } </style> <% end %>
See the Colors documentation for the full list of tokens.
Customising logos and icons
Replace the defaults by placing files with the same names in app/assets/images/layered_ui/ in your host app:
| File | Used for |
|---|---|
logo_light.svg |
Header logo (light theme) |
logo_dark.svg |
Header logo (dark theme) |
icon_light.svg |
Favicon and header icon (light theme) |
icon_dark.svg |
Favicon and header icon (dark theme) |
apple_touch_icon.png |
Apple touch icon |
panel_icon_light.svg |
Panel toggle button (light theme) |
panel_icon_dark.svg |
Panel toggle button (dark theme) |
layered-ui-rails uses two patterns for per-request overrides:
-
Instance variables (
@l_ui_*) - used for small changes like URLs. The engine renders the surrounding markup and just swaps the src/href. -
content_for- used when the full markup needs to change. You supply the complete tag, so you can set classes, attributes, or wrap elements as needed.
For per-request logos (e.g. per-tenant branding), use content_for because the <img> tag itself carries classes that control layout and theme switching:
<% content_for :l_ui_logo_light do %>
<%= image_tag @tenant.logo_light_url, alt: "", class: "l-ui-header__logo l-ui-header__logo--light" %>
<% end %>
<% content_for :l_ui_logo_dark do %>
<%= image_tag @tenant.logo_dark_url, alt: "", class: "l-ui-header__logo l-ui-header__logo--dark" %>
<% end %>To adjust the size of the logo or header icon, override .l-ui-header__logo or .l-ui-header__icon in layered_ui_overrides.css (created by bin/rails generate layered:ui:create_overrides). Commented examples are included in the Tier 3 section of that file.
For per-request icons, set instance variables - the engine renders <link> and <img> tags that only need a URL to vary:
@l_ui_icon_light_url = @tenant.icon_light_url
@l_ui_icon_dark_url = @tenant.icon_dark_url
@l_ui_apple_touch_icon_url = @tenant.apple_touch_icon_url
@l_ui_panel_icon_light_url = @tenant.panel_icon_light_url
@l_ui_panel_icon_dark_url = @tenant.panel_icon_dark_urlSecurity: Rails HTML-escapes URL values, so XSS via attribute injection is mitigated. However, if values are tenant-controlled, validate that they are legitimate URLs - reject
javascript:schemes and ensure values point to expected origins.
Documentation
An online version of the documentation is available at layered-ui-rails.layered.ai.
The latest accessibility audit is available at audits/accessibility/codex-5_3.md.
You can also run the included dummy app locally for development and testing:
git clone https://github.com/layered-ai-public/layered-ui-rails.git
cd layered-ui-rails
bundle install
cd test/dummy && bin/rails db:setup && bin/devThe dummy app serves as both a living style guide and a test harness, with interactive examples and code snippets for every component.
Deploying the dummy app
The dummy app can be deployed with Kamal. Set the required environment variables and deploy from test/dummy:
cd test/dummy
export KAMAL_DEPLOY_IP=<server-ip>
export KAMAL_DEPLOY_DOMAIN=<domain>
export KAMAL_SSH_KEY=<path-to-ssh-key>
kamal deployContributing
This project is still in its early days. We welcome issues, feedback, and ideas - they genuinely help shape the direction of the project. That said, we're holding off on accepting pull requests until after the 1.0 release so we can stay focused on getting the core foundations right. Once we're there, we'd love to open things up to broader contributions. Thanks for your patience and interest!
License
Released under the Apache 2.0 License.
Copyright 2026 LAYERED AI LIMITED (UK company number: 17056830). See NOTICE for attribution details.
Trademarks
The source code is fully open, but the layered.ai name, logo, and brand assets are trademarks of LAYERED AI LIMITED. The Apache 2.0 license does not grant rights to use the layered.ai branding. Forks and redistributions must use a distinct name. See TRADEMARK.md for the full policy.
Contributing
- CLA.md - contributor license agreement



