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 = falsesurface.disabledContainer.

Secondary — Disabled

Kotlin: enabled = falseborder.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 -->