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 = false → surface.disabledContainer.
Secondary — Disabled
Kotlin: enabled = false → border.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);
}