Pure ViewComponent (BEM) Approach

The button is a plain <button> element styled via shared CSS classes. In a Rails app this is rendered by Neem::BEMButtonComponent (Ruby ViewComponent). No Shadow DOM — the global token stylesheet drives everything.

Primary — Default

Maps to Kotlin PrimaryButton. Full-width, 64px height via token.

Secondary — Default

Maps to Kotlin SecondaryButton. 2px outlined border from token.

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 placed inside .neem-button__content before the label.

Secondary — With Trailing Icon

Trailing icon placed inside .neem-button__content after the label.

Usage — HTML Output (BEM Classes)

No JavaScript required — just link the two stylesheets and write BEM classes. Token variables do all the theming work.

<!-- 1. Link stylesheets once (theme.css imports all 3 token layers via @import) -->
<link rel="stylesheet" href="theme.css" />
<link rel="stylesheet" href="components/button.css" />

<!-- 2. Primary button -->
<button class="neem-button neem-button--primary">
  <span class="neem-button__content">
    <span class="neem-button__icon">...svg...</span>
    <span class="neem-button__label">Confirm Payment</span>
  </span>
  <!-- DottedLoadingIndicator (hidden until --loading class is added) -->
  <div class="neem-button__dots" aria-hidden="true">
    <div class="neem-button__dot"></div>
    <div class="neem-button__dot"></div>
    <div class="neem-button__dot"></div>
  </div>
</button>

<!-- Loading state: toggle class via JS or server -->
<button class="neem-button neem-button--primary neem-button--loading">...</button>

<!-- Disabled -->
<button class="neem-button neem-button--secondary" disabled>...</button>

<!-- Theme switch: change the attribute on a parent -->
<body data-neem-theme="green">  <!-- all buttons go green, no class changes -->

Source — bem_button_component.rb (Ruby / ViewComponent)

Server renders a plain <button> with BEM classes. Zero client-side JavaScript needed.

# app/components/neem/bem_button_component.rb
module Neem
  class BEMButtonComponent < ViewComponent::Base
    def initialize(label:, variant: :primary, theme: :blue,
                   disabled: false, loading: false,
                   leading_icon: nil, trailing_icon: nil)
      @label  = label;  @variant = variant.to_sym
      @theme  = theme.to_sym
      @disabled = disabled; @loading = loading
      @leading_icon = leading_icon; @trailing_icon = trailing_icon
    end

    def before_render
      @classes = [
        "neem-button",
        "neem-button--#{@variant}",
        ("neem-button--loading"  if @loading),
        ("neem-button--disabled" if @disabled)
      ].compact.join(" ")
    end
  end
end

# In any Rails .html.erb template:
<%= render Neem::BEMButtonComponent.new(
  label:   "Confirm Payment",
  variant: :primary,
  theme:   :blue
) %>

Source — components/button.css (key rules)

Every value is a Layer 3 alias token. No hex values, no hardcoded sizes.

/* button.css — components layer. References Layer 3 aliases only. */
.neem-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);
}
.neem-button--primary {
  background: var(--neem-surface-primary);  /* Layer 3 → Layer 2 → palette */
  color:      var(--neem-text-on-primary);
}
.neem-button--secondary {
  border: var(--neem-border-width-bold) solid var(--neem-border-secondary);
  color:  var(--neem-text-secondary);
}
.neem-button:disabled {
  background:   var(--neem-surface-disabled-container);
  color:        var(--neem-text-disabled);
  border-color: var(--neem-border-disabled);
}