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.
Page shell
Section titled “Page shell”<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>Layout primitives
Section titled “Layout primitives”<!-- 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.
Buttons
Section titled “Buttons”<!-- 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>Badges (pills)
Section titled “Badges (pills)”<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.
Empty state
Section titled “Empty state”<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>Loader / spinner
Section titled “Loader / spinner”<div class="lw-loader" aria-label="Loading"></div>KPI grid
Section titled “KPI grid”<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>Inputs
Section titled “Inputs”<label class="lw-field"> <span>Email</span></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>Search input
Section titled “Search input”<div class="lw-search"> <svg width="14" height="14" /* magnifier icon */ ></svg> <input type="search" class="lw-input" placeholder="Search invoices…"></div>Toolbar
Section titled “Toolbar”<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>Data table
Section titled “Data table”<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>Modal (in-bundle overlay)
Section titled “Modal (in-bundle overlay)”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.
Utilities
Section titled “Utilities”<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>When you really need custom CSS
Section titled “When you really need custom CSS”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.