Lit Web Component Approach
The <neem-button> is a standards-based custom element built with
Lit 3. It uses Shadow DOM with CSS custom properties
adopted from the global token sheet, so themes and dark mode work automatically.
Primary — Default
Maps to Kotlin PrimaryButton. Full-width, 64dp height.
Secondary — Default
Maps to Kotlin SecondaryButton. 2dp outlined border.
Primary — Loading
Loading state shows DottedLoadingIndicator. Content hidden, dots visible.
Secondary — Loading
Secondary loading state. Same dot animation, outlined style preserved.
Primary — Disabled
Kotlin: enabled = false → surface.disabledContainer.
Secondary — Disabled
Kotlin: enabled = false → border.disabled + text.disabled.
Primary — With Leading Icon
Leading icon via leadingIcon property.
Secondary — With Trailing Icon
Trailing icon via trailingIcon property.
Usage — HTML / Any Framework
Since <neem-button> is a standard Web Component, it works in plain HTML, React, Vue, Angular, or any Rails template.
<!-- Step 1: Import once (auto-registers the custom element) --> <script type="module" src="/src/neem-button.js"></script> <!-- Step 2: Use as native HTML --> <neem-button label="Confirm Payment" variant="primary"></neem-button> <neem-button label="Cancel Action" variant="secondary"></neem-button> <!-- With leading icon --> <neem-button label="Secure Transaction" variant="primary" leadingIcon="<svg viewBox='0 0 24 24'>...</svg>" ></neem-button> <!-- Loading state (shows DottedLoadingIndicator) --> <neem-button label="Saving..." variant="primary" loading></neem-button> <!-- Disabled state --> <neem-button label="Not Allowed" variant="secondary" disabled></neem-button> <!-- Theme is set on a parent element, not on the component itself --> <body data-neem-theme="blue"> <!-- or "green" / "dark" -->
Source — src/neem-button.ts (Lit 3 + TypeScript)
Every visual value comes from a Layer 3 alias token. The component never knows which theme is active.
@customElement('neem-button')
export class NeemButton extends LitElement {
@property({ type: String }) variant: 'primary' | 'secondary' = 'primary';
@property({ type: String }) label = '';
@property({ type: Boolean, reflect: true }) disabled = false;
@property({ type: Boolean, reflect: true }) loading = false;
@property({ type: String }) leadingIcon = '';
@property({ type: String }) trailingIcon = '';
static styles = css`
/* Shadow DOM: CSS custom properties pierce the boundary automatically */
button {
height: var(--neem-spacing-extra-extra-large); /* 64px = Kotlin .height(64.dp) */
border-radius: var(--neem-shapes-large); /* 16px = NeemTheme.shapes.large */
font-size: var(--neem-typography-label-large-size);
font-weight: var(--neem-typography-weights-bold);
letter-spacing: var(--neem-typography-label-large-letter-spacing);
transition: all var(--neem-motion-duration-short) var(--neem-motion-easing-standard);
}
button.primary {
background-color: var(--neem-surface-primary); /* Layer 3 alias → --neem-primary */
color: var(--neem-text-on-primary); /* Layer 3 alias → --neem-on-primary */
}
button.secondary {
border: var(--neem-border-width-bold) solid var(--neem-border-secondary);
color: var(--neem-text-secondary);
}
button:disabled {
background: var(--neem-surface-disabled-container);
color: var(--neem-text-disabled);
border-color: var(--neem-border-disabled);
}
`;
render() {
return html`
<button class=${this.variant} ?disabled=${this.disabled || this.loading}
@click=${this._handleClick}>
<div class="content ${this.loading ? 'hidden' : ''}">
${this.leadingIcon ? html`<span class="icon" .innerHTML=${this.leadingIcon}></span>` : ''}
<span>${this.label}</span>
${this.trailingIcon ? html`<span class="icon" .innerHTML=${this.trailingIcon}></span>` : ''}
</div>
<div class="dots" aria-hidden="true"> <!-- DottedLoadingIndicator -->
<div class="dot"></div><div class="dot"></div><div class="dot"></div>
</div>
</button>
`;
}
}
Token System — Three-Layer Architecture
The component references only Layer 3 aliases. The theme is resolved automatically by the CSS cascade.
/* palette.css — Layer 1: raw values, defined once */
:root {
--neem-palette-cyan20: #6CD2FF;
--neem-palette-green20: #006C45;
}
/* themes.css — Layer 2: brand decisions, one block per theme */
[data-neem-theme="blue"] { --neem-primary: var(--neem-palette-cyan20); }
[data-neem-theme="green"] { --neem-primary: var(--neem-palette-green20); }
/* aliases.css — Layer 3: semantic tokens consumed by components */
:root {
--neem-surface-primary: var(--neem-primary); /* this is all the component ever reads */
--neem-text-on-primary: var(--neem-on-primary);
}
/* Result: switching theme only changes Layer 2. No component is edited. */
<body data-neem-theme="green"> <!-- every neem-button instantly goes green -->