Skip to content

lw-ui — Components Catalog

This page is the copy-paste catalog: every lw-ui component with its markup. Pair it with the lw-ui reference for tokens + philosophy.

<main class="lw-app">
<header class="lw-app-header">
<div class="lw-app-header__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none"
stroke="currentColor" stroke-width="2">
<rect x="2" y="7" width="20" height="14" rx="2"/>
</svg>
</div>
<div class="lw-app-header__title-block">
<h1 class="lw-app-header__title">My App</h1>
<p class="lw-app-header__subtitle">A short tagline</p>
</div>
</header>
<!-- … your content … -->
</main>
<!-- Vertical group with consistent gaps -->
<div class="lw-stack">
<p>First</p>
<p>Second</p>
</div>
<!-- Horizontal group, wraps -->
<div class="lw-row">
<button class="lw-btn">A</button>
<button class="lw-btn">B</button>
</div>
<div class="lw-tabs">
<div class="lw-tabs-list" role="tablist">
<button class="lw-tab" role="tab" aria-selected="true">Active</button>
<button class="lw-tab" role="tab" aria-selected="false">Archive</button>
</div>
<!-- tab content goes here -->
</div>

You manage aria-selected flipping yourself; the styling reacts to it.

<!-- Primary CTA -->
<button class="lw-btn lw-btn--primary">Save</button>
<!-- Secondary / outline -->
<button class="lw-btn">Cancel</button>
<!-- Ghost (transparent until hover) -->
<button class="lw-btn lw-btn--ghost">Retry</button>
<!-- Destructive -->
<button class="lw-btn lw-btn--danger">Delete</button>
<!-- Icon-only -->
<button class="lw-btn lw-btn--icon" aria-label="Refresh">
<svg width="16" height="16" /* */ ></svg>
</button>
<span class="lw-badge">7</span>
<span class="lw-badge lw-badge--info">Info</span>
<span class="lw-badge lw-badge--success">Sent</span>
<span class="lw-badge lw-badge--warning">Pending</span>
<span class="lw-badge lw-badge--danger">Error</span>
<article class="lw-card">
<div class="lw-card-header">
<h3>Customer #4521</h3>
<span class="lw-badge lw-badge--success">Active</span>
</div>
<div class="lw-card-body lw-stack">
<p>Body content.</p>
<div class="lw-row">
<button class="lw-btn lw-btn--primary">Open</button>
<button class="lw-btn lw-btn--ghost">Archive</button>
</div>
</div>
</article>

For a flush-to-edge body (tables, lists), add lw-card-body--flush.

<div class="lw-empty">
<svg width="32" height="32" /* icon */ ></svg>
<p>No invoices yet.</p>
<button class="lw-btn lw-btn--primary">Create one</button>
</div>
<div class="lw-loader" aria-label="Loading"></div>
<div class="lw-kpi-grid">
<div class="lw-kpi lw-kpi--success">
<div class="lw-kpi__icon"></div>
<div class="lw-kpi__label">Revenue</div>
<div class="lw-kpi__value">€ 12,450</div>
<div class="lw-kpi__sub">+18% vs. last month</div>
</div>
<div class="lw-kpi lw-kpi--warning">
<div class="lw-kpi__icon"></div>
<div class="lw-kpi__label">Open quotes</div>
<div class="lw-kpi__value">7</div>
</div>
<!-- variants: lw-kpi--success / --warning / --info / --danger -->
</div>
<label class="lw-field">
<span>Email</span>
<input type="email" class="lw-input" placeholder="[email protected]">
</label>
<label class="lw-field">
<span>Notes</span>
<textarea class="lw-textarea" rows="4"></textarea>
</label>
<label class="lw-field">
<span>Country</span>
<select class="lw-select">
<option>Germany</option>
<option>Austria</option>
</select>
</label>
<div class="lw-search">
<svg width="14" height="14" /* magnifier icon */ ></svg>
<input type="search" class="lw-input" placeholder="Search invoices…">
</div>
<div class="lw-toolbar">
<div class="lw-search">
<input type="search" class="lw-input" placeholder="Search…">
</div>
<div class="lw-row">
<button class="lw-btn">Filter</button>
<button class="lw-btn lw-btn--primary">New</button>
</div>
</div>
<div class="lw-card lw-card-body--flush">
<table class="lw-table">
<thead>
<tr><th>Number</th><th>Client</th><th>Total</th><th>Status</th></tr>
</thead>
<tbody>
<tr>
<td class="lw-mono">A2026-0042</td>
<td>Acme GmbH</td>
<td class="lw-numeric">€ 1,240.00</td>
<td><span class="lw-badge lw-badge--success">Sent</span></td>
</tr>
</tbody>
</table>
</div>

For modals inside your iframe.

<div class="lw-modal" role="dialog" aria-modal="true">
<div class="lw-modal__panel">
<h3>Delete invoice A2026-0042?</h3>
<p>This cannot be undone.</p>
<div class="lw-row" style="justify-content: flex-end;">
<button class="lw-btn">Cancel</button>
<button class="lw-btn lw-btn--danger">Delete</button>
</div>
</div>
</div>

For most “are you sure?” prompts prefer bridge.confirm() — it renders in the platform-host’s overlay layer instead of inside your iframe and behaves consistently with platform UX.

<p class="lw-text-muted">Secondary copy.</p>
<p class="lw-text-secondary">Tertiary copy.</p>
<code class="lw-mono">A2026-0042</code>
<span class="lw-numeric">1,240.00</span>
<span class="lw-truncate" style="max-width: 200px;">A very long subject…</span>

Layout glue lw-ui doesn’t ship — sticky positioning, complex grid templates for your specific data, sub-component composition — goes in your web/styles.css (or app.css). The rule of thumb:

  • ✅ “I need this card to span 2 columns of a grid”
  • ✅ “I need a sticky right panel that stays on screen”
  • ❌ “I need a button with my own colors” → use lw-btn
  • ❌ “I need a tab strip with hover effects” → use lw-tab

Use the var(--_lw-*) tokens whenever you reach for a value:

.my-grid {
display: grid;
grid-template-columns: 1fr 360px;
gap: var(--_lw-space-lg);
}

Hard-coded hex values fight the tenant theme.