Primitive
Button Hybrid
A multi-variant action trigger with built-in loading and disabled states. Works as a CSS-only class set or as the <ren-button> custom element. Meets a 44 px touch target and AA contrast in every variant out of the box.
About
Overview
A button triggers an action — submitting a form, opening a dialog, deleting a record. RenDS gives you one component with seven visual variants (primary, secondary, ghost, outline, danger, link, accent), three sizes, and a built-in loading state that flips the button to aria-busy="true" and disables interaction without you wiring anything.
Every variant ships with a 44 px minimum touch target (Apple HIG), a visible focus ring, hover/active feedback that respects prefers-reduced-motion, and AA contrast in both light and dark mode.
When to use
- To trigger an action on the current page (submit, save, delete, copy, run).
- To open or close an overlay (dialog, popover, menu).
- For a primary CTA, group it with a secondary or ghost button — never two primaries side by side.
When not to use
- For navigation to a different page or a different URL — that's
<a>orren-link. A button that navigates breaks browser conventions (no middle-click open, no copy-link). - For an inline call-to-read-more inside body text — use the
linkvariant only when a button is genuinely needed; otherwise reach forren-link. - For a binary state choice — that's
ren-toggleor a checkbox.
Native button, semantically. The <ren-button> custom element wraps a native <button> in light DOM. Forms still submit, focus still works, screen readers still announce "button". You're not opting out of the platform — you're styling it.
Structure
Anatomy
A button has up to four visible parts. Only the label is mandatory — the rest are optional and combine freely.
Assembled
The whole clickable surface. Sets the variant color, padding, border-radius, and the 44 px touch target. Receives focus.
Optional. Placed before the label, gap-spaced with --space-2. Decorative icons should carry aria-hidden="true".
The visible text. Required for non-icon buttons. For icon-only buttons, omit it and add aria-label on the button itself.
Optional. Same rules as the leading icon. Common cases: a chevron for "more options", an external-link arrow.
Live
Demo
The default button — primary variant, medium size. Tab to it, press Space or Enter to fire the click handler.
<!-- CSS-only -->
<button class="ren-btn ren-btn-primary" type="button">Primary action</button>
<!-- Web Component (same output, attribute-driven) -->
<ren-button variant="primary">Primary action</ren-button>
Two ways to use it, one component. The CSS classes work standalone — you don't need the JS at all. The <ren-button> element exists for two reasons: declarative attributes (variant, loading) you can flip from JS, and built-in ARIA wiring for the loading state.
Catalog
Variants
Pick the variant that matches the action's weight, not its category. Primary for the one most-likely action on the screen, secondary for everything else, danger for destructive, ghost when the button has to disappear into the chrome.
Reference
API
Two parallel surfaces. Pick the one that matches your codebase: CSS classes for static markup, the custom element for declarative attributes you can flip at runtime.
CSS classes
| Class | Effect |
|---|---|
.ren-btn |
Base. Required on every button. Resolves to primary if no variant modifier is present. |
.ren-btn-primary |
Filled accent. The default — also implicit when only .ren-btn is set. |
.ren-btn-secondary |
Filled neutral. The most common companion to a primary. |
.ren-btn-ghost |
No background until hover. For toolbars and dense chrome. |
.ren-btn-outline |
Border only, transparent fill. A lighter secondary. |
.ren-btn-danger |
Destructive actions. Filled red, text-on-red passes AA in both modes. |
.ren-btn-link |
Looks like an inline link but behaves as a button. Use sparingly. |
.ren-btn-sm / .ren-btn-lg |
Size modifiers. Default (no modifier) is medium with a 44 px touch target. |
.ren-btn-icon |
Square aspect ratio for icon-only buttons. Pair with aria-label. |
.ren-btn-full |
Stretches to width: 100%. For mobile CTAs and stepped forms. |
.ren-btn-group |
Wrapper class. Joins adjacent buttons into a segmented control with shared borders. |
[data-loading] |
Attribute, not a class. Shows a spinner, hides the label, sets aria-busy. |
Web Component attributes
Set on the <ren-button> element. All are reactive — flipping them in JS re-renders the underlying button.
| Attribute | Type | Default | Notes |
|---|---|---|---|
variant |
"primary" | "secondary" | "ghost" | "outline" | "danger" | "link" | "primary" | Visual variant. |
size |
"sm" | "md" | "lg" | "md" | Padding and font scale. |
loading |
boolean | false | Adds data-loading + aria-busy="true". |
disabled |
boolean | false | Mirrors the native disabled + sets aria-disabled. |
icon |
boolean | false | Square button for icon-only usage. |
full |
boolean | false | Stretches to 100% width. |
JS properties
The RenButton class exposes getters/setters that mirror the boolean attributes. Useful when you'd rather flip a property than toggle an attribute.
const btn = document.querySelector('ren-button');
btn.loading = true; // shows spinner
btn.disabled = true;
btn.variant = 'danger'; // re-rendersInclusive by default
Accessibility
Every button passes WCAG 2.1 AA in both modes and every variant. The component does most of the work for you, but a few things are still your call.
Keyboard
type="button" is set.
Always set type="button" on buttons that aren't form submitters. The browser default is type="submit", which will submit the nearest enclosing form on Enter — usually not what you want for "Cancel", "Open menu", or any non-submit action.
ARIA wiring
- The button has
role="button"implicitly via the native<button>. No need to set it. - For icon-only buttons, set
aria-labelon the button. Decorative icons inside should carryaria-hidden="true". - For loading state, the component sets
aria-busy="true"automatically. Screen readers will announce the state change. - For disabled state, both
disabledandaria-disabled="true"are set. Disabled buttons are skipped in tab order; if you need a focusable-but-inert button (rare), usearia-disabledalone and handle the click guard yourself. - For toggle buttons (a button that flips between two states), add
aria-pressed="true"or"false". The group example above uses this.
Screen reader announcements
- "Add to cart, button" — for a regular labelled button.
- "Search, button, busy" — when
loadingis set. - "Save, button, dimmed" (or similar) — when
disabledis set. - "Week, button, pressed" — for a toggled button in a group.
Touch and pointer
- Default and large sizes meet the 44 × 44 px minimum touch target. The small size is below that and should only be used in dense desktop UI (toolbars, table cells).
- The press-down feedback (a
scale(0.97)on:active) respectsprefers-reduced-motionvia the semantic motion tokens — it stays subtle for users who request reduced motion. - Hover styles never replace focus styles. A keyboard user always sees a focus ring on the focused button, even when a different button is hovered.
Recipes
Examples
Common patterns that combine the variants above with other RenDS primitives. Copy the markup, swap the labels.
Primary + secondary pair
The classic "confirm and cancel" pairing. Primary on the right (or last), secondary on the left.
<div class="ren-cluster">
<button class="ren-btn ren-btn-secondary" type="button">Cancel</button>
<button class="ren-btn ren-btn-primary" type="button">Save changes</button>
</div>
Destructive confirmation
For irreversible actions. The danger variant signals stakes; pair with secondary, never with another colored variant.
<div class="ren-cluster">
<button class="ren-btn ren-btn-secondary" type="button">Keep account</button>
<button class="ren-btn ren-btn-danger" type="button">Delete account</button>
</div>
Loading from JS
Flip loading on submit, flip it back when the request resolves.
const btn = document.querySelector('ren-button#demo-load');
btn.addEventListener('click', async () => {
btn.loading = true;
try {
await fetch('/api/save', { method: 'POST' });
} finally {
btn.loading = false;
}
});
Icon-only in a toolbar
Ghost icon buttons disappear into chrome until hovered. Always set aria-label.
<div class="ren-cluster">
<button class="ren-btn ren-btn-ghost ren-btn-icon" type="button" aria-label="Bold">
<svg ... aria-hidden="true">...</svg>
</button>
<!-- repeat for italic, underline -->
</div>
Segmented control
Three buttons in a group, one pressed. Manage aria-pressed from JS as the user clicks.
<div class="ren-btn-group" role="group" aria-label="View by">
<button class="ren-btn ren-btn-secondary" type="button">Day</button>
<button class="ren-btn ren-btn-secondary" type="button" aria-pressed="true">Week</button>
<button class="ren-btn ren-btn-secondary" type="button">Month</button>
</div>
Full-width on mobile
Stretch the primary CTA on small screens for comfortable thumb reach.
<button class="ren-btn ren-btn-primary ren-btn-lg ren-btn-full" type="button">
Continue
</button>