Components
Foundations
Typography, spacing and reading rhythm shared by every app.
Choose a role, then arrange the content
Text styles pair size, line height, weight and tracking. Heading level describes the document structure;
size: describes its appearance. Parents set space between children with stacks, inline groups and sections.
Use <code> for inline references with a monospace font and subtle background.
Use reading styles for articles
lc_prose styles rendered HTML with a 65ch reading measure, 1.75 line height and proportional block spacing.
Enable @plugin "@tailwindcss/typography"; in the host Tailwind input. The standalone Rails CLI includes the plugin.
Mark embedded controls or cards not-prose to keep their component styles.
Theme colors
These eleven colors are the everyday theme settings. Defaults work without overrides.
Names describe their purpose: bg-muted for a card, text-muted-foreground for a hint,
border-border for a divider. Values below follow the selected palette and light or dark mode.
Change --primary to update actions, links, selection and focus together.
Soft backgrounds for primary and status colors are generated automatically.
See the theming guide for examples.
| CSS property | Value | Used for |
|---|---|---|
--background |
|
Page background |
--foreground |
|
Main text and headings |
--card |
|
Cards and grouped content |
--card-foreground |
|
Text on cards |
--card-input |
|
Fields on cards; defaults to --input |
--card-secondary |
|
Secondary buttons on cards; defaults to --secondary |
--popover |
|
Menus, popovers and dialogs |
--popover-foreground |
|
Text on menus, popovers and dialogs |
--primary |
|
Primary actions, links, selected states and keyboard focus |
--primary-foreground |
|
Text and icons on a solid primary background |
--secondary |
|
Secondary buttons |
--secondary-foreground |
|
Text on secondary buttons |
--muted |
|
Subtle backgrounds: badges, code, skeletons, table headers |
--muted-foreground |
|
Secondary text, hints and placeholders |
--accent |
|
Hovered rows, menu items and inline code |
--accent-foreground |
|
Text on accent backgrounds |
--destructive |
|
Errors and destructive actions |
--success |
|
Successful actions and positive feedback |
--warning |
|
Warnings and items needing attention |
--border |
|
Dividers and outlines |
--input |
|
Input and toggle fills |
--ring |
|
Keyboard focus ring; unset, it follows --primary |
Typography, spacing and appearance
Use these settings when you need to change typography, spacing or rendering details.
Set a property on a page wrapper for a local change, or on :root for the whole app.
Load overrides after the engine stylesheet. Japanese text follows lang="ja".
.report-page {
--primary: oklch(46% 0.14 155);
--lc-inset: 1.5rem;
--lc-density: 1.125;
--text-title: 2rem;
}
| CSS property | Value | Used for |
|---|---|---|
| Fonts | ||
--font-display |
var(--font-sans)
|
Heading font family |
--font-sans |
Tailwind's sans stack
|
Body font family |
--font-mono |
Tailwind's mono stack
|
Code font family |
--font-eyebrow |
var(--font-mono)
|
Eyebrow font family; Japanese text uses --font-sans |
--font-japanese |
"Hiragino Sans", "Yu Gothic", "YuGothic", "Meiryo", ui-sans-serif, system-ui, sans-serif
|
Font family for Japanese text |
| Type roles | ||
--text-display |
2.25rem / 2.5rem
|
Hero text (text-display) |
--text-title |
1.875rem / 2.25rem
|
Page title (text-title) |
--text-heading |
1.25rem / 1.75rem
|
Section title (text-heading) |
--text-subheading |
1.125rem / 1.5rem
|
Card and dialog title (text-subheading) |
--text-body |
1rem / 1.5rem
|
Body text (text-body) |
--text-small |
0.875rem / 1.25rem
|
Hints and secondary text (text-small) |
--text-label |
0.875rem / 1.25rem
|
Labels, buttons and menu items (text-label) |
--text-caption |
0.75rem / 1rem
|
Metadata and table headers (text-caption) |
--text-eyebrow |
0.75rem / 1rem
|
Uppercase kickers and column labels (text-eyebrow) |
| Spacing | ||
--spacing-tight |
0.25rem
|
Icon and label (gap-tight) |
--spacing-related |
0.5rem
|
Label and control, button groups (gap-related) |
--spacing-heading |
0.75rem
|
Heading and its content (gap-heading) |
--spacing-content |
1rem
|
Blocks and fields (gap-content) |
--spacing-inset |
var(--lc-inset)
|
Container padding and the gaps between its blocks (p-inset) |
--spacing-section |
2rem
|
Page sections (gap-section) |
| Layout and reading | ||
--lc-gutter |
clamp(1rem, 3vw, 1.5rem)
|
Page container side padding (px-gutter) |
--container-page |
75rem
|
Maximum page container width (max-w-page) |
--container-form |
32rem
|
Maximum form container width (max-w-form) |
--container-reading |
65ch
|
Maximum reading width (max-w-reading) |
--lc-reading-size |
1rem
|
Reading text size |
--lc-reading-leading |
1.75
|
Reading text line height |
--lc-reading-flow |
1.25em
|
Space between reading blocks |
--lc-reading-heading-leading |
1.25
|
Reading heading line height. Japanese headings use 1.4. |
| Shape and density | ||
--radius |
0.5rem
|
Every corner radius; rounded-sm to rounded-3xl and rounded-pill scale from it |
--lc-density |
1
|
Multiplies semantic spacing and control heights |
--lc-inset |
1.25rem
|
Card, dialog and popover padding |
| Controls | ||
--lc-control-sm |
2rem
|
Small control height (h-control-sm) |
--lc-control-md |
2.5rem
|
Default control height (h-control-md) |
--lc-control-lg |
3rem
|
Large control height (h-control-lg) |
--lc-input-hover |
|
Hovered select background |
| Shadows | ||
--lc-shadow-sm |
|
Small surface shadow |
--lc-shadow |
|
Default floating surface shadow |
--lc-shadow-lg |
|
Large floating surface shadow |
| Surfaces | ||
--lc-glass-fill |
|
Translucent panel fill |
--lc-glass-edge |
|
Translucent panel border |
| Overlays | ||
--lc-scrim |
|
Dialog backdrop |
--lc-scrim-sheet |
|
Sheet backdrop |
| Motion | ||
--lc-sheet-duration |
|
Sheet opening and closing time |
--lc-motion-control |
|
Button interaction time |
Feedback follows the action
Buttons respond to hover and press automatically. Copy buttons transition their label and icon after the clipboard write succeeds. The same button keeps focus and reserves room for every label, so nearby content stays still. Try the controls below with a mouse, keyboard, or touch.
Share timing across your app
Override these CSS variables after the engine import, or on a container to tune one area.
Feedback duration controls the visual transition. The copy button's reset_after: controls how long confirmation remains visible.
--lc-motion-quick- 140ms · icon controls
--lc-motion-control- 180ms · buttons
--lc-motion-feedback- 160ms · label and icon swaps
--lc-press-scale- 0.96 · pressed icon controls
--lc-ease-out- Shared control easing
Respect reduced motion
When the operating system requests reduced motion, controls skip lift and press scaling, and copy feedback changes immediately. Labels, icons, focus, and success announcements still work. Other component animations use the theme's existing reduced-motion handling.
Connect application actions to real results
Your application owns saving and submitting. Show loading while work is pending, success after it finishes, and an error when it fails.
Compose lc_button with lc_spinner and aria-busy for loading feedback.
Keep timings in CSS and expose interaction state through data or ARIA attributes in your own controllers.
Blocks reuse the library's controls and supply their application behavior.
Usage
After the one-time setup, call the helper directly from ERB. No component import is needed.
<div class="grid gap-5">
<%= lc_heading "Weekend in Kyoto", level: 2, size: :title %>
<%= lc_heading "Shared expenses", level: 3, size: :heading %>
<%= lc_heading "Dinner with friends", level: 4, size: :subheading %>
<%= lc_text do %>Use <code>lc_stack</code> to keep related content together.<% end %>
<%= lc_text "Add a receipt or a short note for your group.", size: :small, tone: :muted %>
<%= lc_text "Updated a few minutes ago", size: :caption, tone: :muted %>
</div>
Examples
Text styles
Weekend in Kyoto
Shared expenses
Dinner with friends
Use lc_stack to keep related content together.
Add a receipt or a short note for your group.
Updated a few minutes ago
<div class="grid gap-5">
<%= lc_heading "Weekend in Kyoto", level: 2, size: :title %>
<%= lc_heading "Shared expenses", level: 3, size: :heading %>
<%= lc_heading "Dinner with friends", level: 4, size: :subheading %>
<%= lc_text do %>Use <code>lc_stack</code> to keep related content together.<% end %>
<%= lc_text "Add a receipt or a short note for your group.", size: :small, tone: :muted %>
<%= lc_text "Updated a few minutes ago", size: :caption, tone: :muted %>
</div>
Japanese text
京都で過ごす週末、みんなの旅の記録
宿泊費や食事代をまとめて記録。誰がいくら支払ったか、グループのみんなで確認できます。
レシートやメモを追加して、旅の思い出も一緒に残しましょう。
最終更新:数分前
<div lang="ja" class="grid max-w-sm gap-4">
<%= lc_heading "京都で過ごす週末、みんなの旅の記録", level: 3, size: :title %>
<%= lc_text "宿泊費や食事代をまとめて記録。誰がいくら支払ったか、グループのみんなで確認できます。" %>
<%= lc_text "レシートやメモを追加して、旅の思い出も一緒に残しましょう。", size: :small, tone: :muted %>
<%= lc_text "最終更新:数分前", size: :caption, tone: :muted %>
</div>
Eyebrow
<div class="grid gap-related">
<%= lc_text "Product", size: :eyebrow, tone: :muted %>
<%= lc_heading "Split costs with anyone", level: 3, size: :heading %>
</div>
<div lang="ja" class="grid gap-related">
<%= lc_text "プロダクト", size: :eyebrow, tone: :muted %>
<%= lc_heading "誰とでも費用を割り勘", level: 3, size: :heading %>
</div>
Page rhythm
Recent expenses
Train tickets
Members
Everyone who can see this workspace.
<%= lc_stack gap: :section, class: "w-full max-w-xl" do %>
<%= lc_section title: "Recent expenses", actions: capture { lc_button("Add expense", icon: "plus", size: :sm) } do %>
<%= lc_card do %>
<%= lc_inline class: "flex-nowrap justify-between" do %>
<%= lc_text "Dinner with friends" %>
<%= lc_text "¥8,400", class: "shrink-0 tabular-nums" %>
<% end %>
<% end %>
<%= lc_card do %>
<%= lc_text "Train tickets" %>
<% end %>
<% end %>
<%= lc_stack as: :section, gap: :heading do %>
<%= lc_section_header do %>
<%= lc_stack gap: :tight do %>
<%= lc_heading "Members" %>
<%= lc_text "Everyone who can see this workspace.", size: :small, tone: :muted %>
<% end %>
<% end %>
<%= lc_card do %>
<%= lc_inline do %>
<%= lc_avatar "AK", class: "bg-primary" %>
<%= lc_stack gap: :tight do %>
<%= lc_text "Alex Kim" %>
<%= lc_text "Joined today", size: :small, tone: :muted %>
<% end %>
<% end %>
<% end %>
<% end %>
<% end %>
Responsive containers
Plan your next trip
Travel details
Kyoto weekend
Keep shared costs and travel notes together.
Shared costs
No expenses yet
<%= lc_container do %>
<%= lc_stack gap: :section do %>
<%= lc_heading "Plan your next trip", level: 2, size: :title %>
<div class="grid min-w-0 gap-8 md:grid-cols-2">
<%= lc_section title: "Travel details", level: 3 do %>
<%= lc_card do %>
<%= lc_stack do %>
<%= lc_text "Kyoto weekend", size: :body %>
<%= lc_text "Keep shared costs and travel notes together.", size: :small, tone: :muted %>
<%= lc_inline do %>
<%= lc_button "Invite friends", size: :sm %>
<%= lc_button "View itinerary", variant: :ghost, size: :sm %>
<% end %>
<% end %>
<% end %>
<% end %>
<%= lc_section title: "Shared costs", level: 3 do %>
<%= lc_card do %>
<%= lc_text "No expenses yet", size: :body %>
<% end %>
<% end %>
</div>
<% end %>
<% end %>
Reading content
Make room for the trip
A shared plan is easier to follow when each idea has room to breathe. Keep the essentials close and give a new topic a little more space.
Start with one clear decision, name the plan kyoto-weekend, then choose what comes next.
- Choose a place to stay.
- Set aside time for a slow morning.
- Leave an afternoon open.
The best part of a plan is the space it leaves for a surprise.
Ready to begin?
Buttons and other app controls can opt out of reading styles.
<%= lc_prose do %>
<h2>Make room for the trip</h2>
<p>A shared plan is easier to follow when each idea has room to breathe. Keep the essentials close and give a new topic a little more space.</p>
<p>Start with <strong>one clear decision</strong>, name the plan <code>kyoto-weekend</code>, then <a href="#reading-next">choose what comes next</a>.</p>
<ul>
<li>Choose a place to stay.</li>
<li>Set aside time for a slow morning.</li>
<li>Leave an afternoon open.</li>
</ul>
<blockquote><p>The best part of a plan is the space it leaves for a surprise.</p></blockquote>
<h3 id="reading-next">Ready to begin?</h3>
<p>Buttons and other app controls can opt out of reading styles.</p>
<div class="not-prose mt-5">
<%= lc_button "Create a plan" %>
</div>
<% end %>
Japanese reading
旅の計画に、少しの余白を。
みんなで予定を考えるときは、大切なことから順番に決めていきましょう。読みやすい行間と段落の間隔が、長い文章をたどる手助けになります。
最初に決めておきたいこと
- 集合する場所と時間
- みんなで分ける費用
- 自由に過ごす時間
予定が変わったら、グループにひとこと。少しの共有で、旅がもっと気軽になります。
何もしない時間も、旅の楽しみのひとつです。
<%= lc_prose lang: "ja" do %>
<h2>旅の計画に、少しの余白を。</h2>
<p>みんなで予定を考えるときは、大切なことから順番に決めていきましょう。読みやすい行間と段落の間隔が、長い文章をたどる手助けになります。</p>
<h3>最初に決めておきたいこと</h3>
<ul>
<li>集合する場所と時間</li>
<li>みんなで分ける費用</li>
<li>自由に過ごす時間</li>
</ul>
<p>予定が変わったら、<a href="#reading-japanese-note">グループにひとこと</a>。少しの共有で、旅がもっと気軽になります。</p>
<blockquote id="reading-japanese-note"><p>何もしない時間も、旅の楽しみのひとつです。</p></blockquote>
<% end %>
Hover and press
<%= lc_button "Hover or press", icon: "plus" %>
<%= lc_button "Secondary", variant: :secondary %>
<%= lc_icon_button "Add item", icon: "plus" %>
Motion feedback
<%= lc_copy_button "Copy link", value: "https://ui.lostcoords.com", copied_label: "Link copied", reset_after: 2500 %>
<%= lc_copy_button "Copy link", value: "https://ui.lostcoords.com", icon_only: true, variant: :ghost %>
API reference
Helpers render semantic HTML and include their interaction wiring. Pass native attributes through class:, data:, aria: and the usual Rails options. Blocks use captured ERB content.
lc_stack
lc_stack(gap: :content, as: :div, **attributes, &block)
| Input | Default | Values |
|---|---|---|
| gap | :content | — |
| as | :div | — |
| attributes | {} | Native HTML attributes; class:, data: and aria: merge with defaults |
| block | Captured block | Captured Rails ERB content |
lc_inline
lc_inline(gap: :related, as: :div, **attributes, &block)
| Input | Default | Values |
|---|---|---|
| gap | :related | — |
| as | :div | — |
| attributes | {} | Native HTML attributes; class:, data: and aria: merge with defaults |
| block | Captured block | Captured Rails ERB content |
lc_container
lc_container(size: :page, **attributes, &block)
| Input | Default | Values |
|---|---|---|
| size | :page | — |
| attributes | {} | Native HTML attributes; class:, data: and aria: merge with defaults |
| block | Captured block | Captured Rails ERB content |
lc_section
lc_section(title:, level: 2, actions: nil, **attributes, &block)
| Input | Default | Values |
|---|---|---|
| title | Required | — |
| level | 2 | — |
| actions | nil | — |
| attributes | {} | Native HTML attributes; class:, data: and aria: merge with defaults |
| block | Captured block | Captured Rails ERB content |
lc_section_header
lc_section_header(as: :header, **attributes, &block)
| Input | Default | Values |
|---|---|---|
| as | :header | — |
| attributes | {} | Native HTML attributes; class:, data: and aria: merge with defaults |
| block | Captured block | Captured Rails ERB content |
lc_heading
lc_heading(content = nil, level: 2, size: :heading, trim: false, **attributes, &block)
| Input | Default | Values |
|---|---|---|
| content | nil | — |
| level | 2 | — |
| size | :heading | — |
| trim | false | true · false |
| attributes | {} | Native HTML attributes; class:, data: and aria: merge with defaults |
| block | Captured block | Captured Rails ERB content |
lc_text
lc_text(content = nil, size: :body, tone: :default, trim: false, as: :p, **attributes, &block)
| Input | Default | Values |
|---|---|---|
| content | nil | — |
| size | :body | — |
| tone | :default | — |
| trim | false | true · false |
| as | :p | — |
| attributes | {} | Native HTML attributes; class:, data: and aria: merge with defaults |
| block | Captured block | Captured Rails ERB content |
lc_prose
lc_prose(**attributes, &block)
| Input | Default | Values |
|---|---|---|
| attributes | {} | Native HTML attributes; class:, data: and aria: merge with defaults |
| block | Captured block | Captured Rails ERB content |
The examples also use lc_button, lc_card, lc_avatar, lc_icon_button, and lc_copy_button. Each is documented on its own page.
Styling and extension
Use class: to merge utilities, native attributes for application behavior, and the theming guide for the shared palette. For custom markup, the advanced class helpers expose the same styles, including glass surfaces and class merging.