Skip to content
Components

Foundations

Typography, spacing and reading rhythm shared by every app.

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.

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.

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.

Theme colors
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

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;
}

Style settings
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

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.

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

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.

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.

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>

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>

京都で過ごす週末、みんなの旅の記録

宿泊費や食事代をまとめて記録。誰がいくら支払ったか、グループのみんなで確認できます。

レシートやメモを追加して、旅の思い出も一緒に残しましょう。

最終更新:数分前

<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>
<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>

Train tickets

<%= 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 %>

Plan your next trip

Kyoto weekend

Keep shared costs and travel notes together.

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 %>

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 %>

旅の計画に、少しの余白を。

みんなで予定を考えるときは、大切なことから順番に決めていきましょう。読みやすい行間と段落の間隔が、長い文章をたどる手助けになります。

最初に決めておきたいこと

  • 集合する場所と時間
  • みんなで分ける費用
  • 自由に過ごす時間

予定が変わったら、グループにひとこと。少しの共有で、旅がもっと気軽になります。

何もしない時間も、旅の楽しみのひとつです。

<%= 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 %>
<%= lc_button "Hover or press", icon: "plus" %>
<%= lc_button "Secondary", variant: :secondary %>
<%= lc_icon_button "Add item", icon: "plus" %>
<%= 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 %>

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)
InputDefaultValues
gap:content—
as:div—
attributes{}Native HTML attributes; class:, data: and aria: merge with defaults
blockCaptured blockCaptured Rails ERB content

lc_inline

lc_inline(gap: :related, as: :div, **attributes, &block)
InputDefaultValues
gap:related—
as:div—
attributes{}Native HTML attributes; class:, data: and aria: merge with defaults
blockCaptured blockCaptured Rails ERB content

lc_container

lc_container(size: :page, **attributes, &block)
InputDefaultValues
size:page—
attributes{}Native HTML attributes; class:, data: and aria: merge with defaults
blockCaptured blockCaptured Rails ERB content

lc_section

lc_section(title:, level: 2, actions: nil, **attributes, &block)
InputDefaultValues
titleRequired—
level2—
actionsnil—
attributes{}Native HTML attributes; class:, data: and aria: merge with defaults
blockCaptured blockCaptured Rails ERB content

lc_section_header

lc_section_header(as: :header, **attributes, &block)
InputDefaultValues
as:header—
attributes{}Native HTML attributes; class:, data: and aria: merge with defaults
blockCaptured blockCaptured Rails ERB content

lc_heading

lc_heading(content = nil, level: 2, size: :heading, trim: false, **attributes, &block)
InputDefaultValues
contentnil—
level2—
size:heading—
trimfalsetrue · false
attributes{}Native HTML attributes; class:, data: and aria: merge with defaults
blockCaptured blockCaptured Rails ERB content

lc_text

lc_text(content = nil, size: :body, tone: :default, trim: false, as: :p, **attributes, &block)
InputDefaultValues
contentnil—
size:body—
tone:default—
trimfalsetrue · false
as:p—
attributes{}Native HTML attributes; class:, data: and aria: merge with defaults
blockCaptured blockCaptured Rails ERB content

lc_prose

lc_prose(**attributes, &block)
InputDefaultValues
attributes{}Native HTML attributes; class:, data: and aria: merge with defaults
blockCaptured blockCaptured 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.

API JSON · LLM reference