R RenDS v0.13.0

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> or ren-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 link variant only when a button is genuinely needed; otherwise reach for ren-link.
  • For a binary state choice — that's ren-toggle or 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

Container

The whole clickable surface. Sets the variant color, padding, border-radius, and the 44 px touch target. Receives focus.

Leading icon

Optional. Placed before the label, gap-spaced with --space-2. Decorative icons should carry aria-hidden="true".

Label

The visible text. Required for non-icon buttons. For icon-only buttons, omit it and add aria-label on the button itself.

Trailing icon

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.

Variant
Size
State
Icon-only
Full width
Group

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-renders

Inclusive 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

Tab Moves focus to the button (skips disabled buttons).
Space Fires the click handler. Standard native behavior.
Enter Fires the click handler. Inside a form, also submits the form unless 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-label on the button. Decorative icons inside should carry aria-hidden="true".
  • For loading state, the component sets aria-busy="true" automatically. Screen readers will announce the state change.
  • For disabled state, both disabled and aria-disabled="true" are set. Disabled buttons are skipped in tab order; if you need a focusable-but-inert button (rare), use aria-disabled alone 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 loading is set.
  • "Save, button, dimmed" (or similar) — when disabled is 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) respects prefers-reduced-motion via 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.

Save
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>