Skip to content

lw-ui CSS Framework

lw-ui.css is a CSS-only framework shipped with @linkworld_ai/sdk-browser. It gives partner-app bundles a polished baseline that automatically inherits the active tenant theme (colors, spacing, fonts, radii, shadows) via the --lw-* tokens that AppFrameHost injects.

Available since sdk-browser 0.4.0.

What you need → which class. Use this as a cheat sheet.

NeedClassNotes
Page shelllw-appTop-level <main>
Page headerlw-app-header + lw-app-header__title / __subtitle / __iconStandard top-bar
Vertical grouplw-stackdisplay: flex; flex-direction: column; gap: …
Horizontal grouplw-rowFlex row with gap, wraps
Card / panellw-card + lw-card-header + lw-card-bodySurfaced container
Tabslw-tabs + lw-tabs-list + lw-tabDrive aria-selected="true" yourself
Buttonlw-btn + lw-btn--primary / --ghost / --danger / --icon
Pill / chiplw-badge + lw-badge--success / --warning / --danger / --infoCounts, tags, status
Inputlw-input (or lw-textarea, lw-select)Inside a lw-field for label + hint
KPI cardlw-kpi + lw-kpi--success / etc. inside lw-kpi-gridDashboard metrics
Empty statelw-emptyCentered “nothing here” placeholder
Loaderlw-loaderSpinner
Search inputlw-searchSearch-shaped variant of lw-input
Toolbarlw-toolbarHeader strip with controls
Modallw-modalIn-bundle overlay (NOT bridge.modal, that’s a sub-iframe)
Data tablelw-tableThemed table
Muted textlw-text-muted / lw-text-secondaryColor tokens
Mono / numericlw-mono / lw-numericFont tweaks
Truncate textlw-truncateOne-line ellipsis
  • Don’t define your own theme tokens. --lw-bg, --lw-surface, --lw-text etc. are injected at runtime by the parent shell. Re-declaring them with hard-coded hex values fights the tenant’s chosen theme.
  • Don’t roll your own .my-tab / .my-button / .my-card. Use lw-tab / lw-btn / lw-card. Custom rules fall behind whenever the platform refines its visual language.
  • Don’t display: flex !important on a [hidden] element to “fix” visibility. [hidden] { display: none !important } keeps the HTML attribute behaviour.
  • Don’t pull theme variables from localStorage or guess from prefers-color-scheme. The bridge already injects the active theme on :root; just use the lw-* classes.
  • Don’t import a CSS framework like Tailwind/Bootstrap/MUI alongside lw-ui. They’ll fight over reset rules and the cascade gets messy. lw-ui is the framework.

The bundle iframe is sandboxed and cross-origin — it cannot import platform React components like <Card> or <Tabs>. lw-ui solves the visual half of that gap with a small, JS-free CSS file. Apps stay vanilla and self-contained; the polish comes from class names.

lw-ui.css ships in the sdk-browser dist. The recommended path is to vendor the SDK into your bundle (so it survives the strict CDN CSP) and reference it relatively:

<link rel="stylesheet" href="vendor/sdk-browser/lw-ui.css">

If you import sdk-browser via npm, you can also reference it via the package export:

import '@linkworld_ai/sdk-browser/lw-ui.css'

Every rule in lw-ui reads from --lw-* CSS custom properties with sensible fallbacks. When the bundle is loaded inside the platform shell, AppFrameHost injects the active theme onto :root. Standalone (e.g. vite dev) the fallbacks produce a usable dark UI.

Token groupExamples
Colors--lw-bg, --lw-surface, --lw-text, --lw-text-muted, --lw-border, --lw-accent
Spacing--lw-space-xs (4) … --lw-space-2xl (32)
Radii--lw-radius-sm, --lw-radius-md, --lw-radius-lg, --lw-radius-full
Fonts--lw-font-sans, --lw-font-mono
Shadows--lw-shadow-sm, --lw-shadow-md

All classes start with lw- to avoid colliding with app-specific styles. Specificity is intentionally low (single-class selectors) so app-level overrides win without !important.

<main class="lw-app">
<header class="lw-app-header">
<div class="lw-app-header__icon">📦</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>
<div class="lw-stack">…vertical gap stack…</div>
<div class="lw-row">…horizontal gap row…</div>
</main>
<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>
<!-- repeat for warning / info / danger / neutral -->
</div>

Tone modifiers: lw-kpi--success, lw-kpi--warning, lw-kpi--info, lw-kpi--danger. Omit for neutral.

<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 handle the click → state → aria-selected flip yourself; the styling reacts to aria-selected="true".

<div class="lw-toolbar">
<div class="lw-search">
<svg class="lw-search__icon"></svg>
<input type="search" placeholder="Search…" />
</div>
<select class="lw-select"></select>
<button class="lw-btn lw-btn--primary">+ New</button>
</div>
<button class="lw-btn">Default</button>
<button class="lw-btn lw-btn--primary">Primary</button>
<button class="lw-btn lw-btn--ghost">Ghost</button>
<button class="lw-btn lw-btn--danger">Delete</button>
<button class="lw-btn lw-btn--icon"></button>
<label class="lw-field">
<span class="lw-field__label">Email</span>
<input class="lw-input" type="email" />
<span class="lw-field__hint">We'll never share this.</span>
</label>
<select class="lw-select"></select>
<textarea class="lw-textarea"></textarea>
<div class="lw-card">
<div class="lw-card-header">
<h3>Heading</h3>
<button class="lw-btn lw-btn--ghost">Edit</button>
</div>
<div class="lw-card-body">
Content
</div>
</div>

For tables that span the full card width, use lw-card-body--flush to drop the body padding.

<table class="lw-table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th class="lw-numeric">Amount</th>
</tr>
</thead>
<tbody>
<tr class="lw-table__row--clickable">
<td>Acme Corp</td>
<td><span class="lw-badge lw-badge--success">Paid</span></td>
<td class="lw-numeric">€ 1,240.00</td>
</tr>
</tbody>
</table>
<span class="lw-badge">Default</span>
<span class="lw-badge lw-badge--success">Active</span>
<span class="lw-badge lw-badge--warning">Pending</span>
<span class="lw-badge lw-badge--info">Sent</span>
<span class="lw-badge lw-badge--danger">Cancelled</span>
<div class="lw-empty">
<div class="lw-empty__icon">📄</div>
<p class="lw-empty__title">No documents yet.</p>
<p class="lw-empty__body">Create one with the button above or via chat.</p>
</div>
<span class="lw-loader" aria-label="Loading"></span>
ClassEffect
lw-text-mutedApply --lw-text-muted color
lw-text-secondaryApply --lw-text-secondary color
lw-numericRight-align + tabular numerals
lw-monoMonospace family
lw-truncateOne-line ellipsis

Anything not covered by lw-ui is yours to style. Tip: continue to consume the same --lw-* tokens in your own CSS so the result still inherits the active tenant theme:

.my-custom-thing {
background: var(--lw-surface, #11141a);
border: 1px solid var(--lw-border, #232733);
}

Real bundles to read end-to-end as you onboard:

  • examples/office-assistant/ — KPI grid + tabs + toolbar + polished list. Use as a reference for dashboard-shaped apps.
  • examples/inbox-manager/ — sub-tabs (lw-tab) for activity buckets, lw-card rows with lw-badge pills, lw-btn--ghost retry actions. Reference for activity-feed apps. Settings live in install_settings (rendered by the platform’s chrome.panel — NO custom settings form needed in the iframe).

The Office Assistant SDK port (examples/office-assistant/) shows KPI grid + tabs + toolbar + polished list, all built on lw-ui classes plus a small app.css for OA-specific bits (detail-view layout, line-items table).