Skip to main content

rc-button

Progressive-enhancement action wrapper for a direct native <button> or <a href> child, with structural state affordances for buttons.

rc-button keeps the real button in light DOM for forms, labels, keyboard behavior, and no-JavaScript fallback. The custom element adds reflected state attributes, icon/label presence hooks, and overlay parts for state-layer or progress effects.

Package
@rcarls/rc-button
Element
<rc-button>
Native dependency
Direct-child native button or anchor
State model
Controlled or uncontrolled toggle state
Main events
rc-button-toggle

Installation​

npm install @rcarls/rc-button
import '@rcarls/rc-button/define';

Markup​

<rc-button toggle default-selected>
<button type="button">
<span data-rc-button-icon aria-hidden="true">♡</span>
<span data-rc-button-selected-icon aria-hidden="true">♥</span>
<span data-rc-button-label>Save</span>
</button>
</rc-button>

Place icons, selected icons, progress-adjacent content, and visible labels inside the native button. rc-button never moves author nodes.

Mark custom progress-adjacent content with data-rc-button-progress so it is excluded from label detection, the same way data-rc-button-icon and data-rc-button-selected-icon are.

A direct <a href> child gets the same visual/layout styling (border, padding, background, full-width) for a real navigation action styled as a button, without losing native link affordances (open in new tab, copy link):

<rc-button class="rc-button--outlined">
<a href="/support">Buy me a coffee</a>
</rc-button>

icon-only sizing, the touch-target hit-slop, and the toggle/disabled/pending/progress states stay button-only — those assume a real HTMLButtonElement.

States​

Use reflected attributes such as pending, progress, selected, icon-only, and full-width as declarative hooks. pending and progress disable the native button and expose aria-busy when the author has not set it. Use pending or boolean progress for an indeterminate affordance. Pair progress with progress-value for a determinate percentage from 0 through 100; out-of-range values are clamped visually.

Use [data-rc-button-icon] and [data-rc-button-selected-icon] inside the button for theme-driven selected icon switching.

Add toggle to opt into the APG toggle-button pattern. In uncontrolled usage, default-selected supplies the initial state and user activation updates selected, aria-pressed, and the displayed icon. In controlled usage, set selected and update it from rc-button-toggle; the event's selected detail contains the requested next state. Host property writes are silent. Removing toggle removes component-owned toggle semantics.

const favoriteButton = document.querySelector('rc-button[toggle]');

favoriteButton.addEventListener('rc-button-toggle', (event) => {
favoriteButton.selected = event.detail.selected;
});

An icon-only button that visually renders smaller than the accessible touch-target minimum (MD3's 40dp "small" icon button, for example) grows its own host to reserve that minimum, keeping the visible button centered inside it; the actual larger hit region comes from an invisible pseudo-element on the native button itself. That reserved space is symmetric by default, which is right when the button has real neighbors on both sides.

At an actual edge, such as the last icon in a toolbar's trailing group, that reserved space just adds dead space past the button before the container's own edge padding. --rc-button-touch-target-overlap-inline-start and --rc-button-touch-target-overlap-inline-end are zero by default; a theme or consumer sets one to give that space back on the given side instead: the visible button shifts to sit flush with the edge, while the invisible hit region keeps its full accessible size, now overlapping the container's own padding instead of sitting outside it. This is a plain token, not a mode rc-button switches on its own — whether a button sits at an edge that warrants it is something only the surrounding layout (a theme's dense variant, or the consumer's own CSS) knows.

.trailing-actions rc-button:last-child {
--rc-button-touch-target-overlap-inline-end: 8px;
}

Only set this on a button that genuinely has no neighbor on the overlapping side — otherwise the two buttons' touch targets overlap each other instead of a container's padding.

Accessibility​

Keep the native button's accessible label unchanged between states. The icons may change because they are presentational and marked aria-hidden, while the visible “Save recipe” label remains stable as required by the APG button pattern.

rc-button writes aria-pressed="true" or "false" only after toggle opts the native child into toggle-button semantics. Enter and Space continue to use the native button's activation behavior, and focus remains on the button.

Without a theme, the direct child keeps its native button appearance. The wrapper adds only layout and behavior until component tokens or a packaged theme opt into decorative styling.

Substrate supplies hover, focus, and pressed state layers, and presents determinate progress as a numeric percentage. Material supplies the same interaction states, adds a reduced-motion-aware ripple originating at the pointer or touch position, and uses a compact circular progress affordance.

Live demo​

The Save recipe button is controlled: both native button activation and the Selected checkbox update the same selected state.

API​

Properties

PropertyMarkupTypeDefaultDescription
disableddisabledbooleanfalseMirror disabled state to the native child.
pendingpendingbooleanfalseShow an indeterminate progress affordance and disable the native button.
progressprogressbooleanfalseShow a progress affordance and disable the native button.
progressValueprogress-valuenumber | undefinedNot specifiedOptional determinate progress percentage, clamped from 0 through 100.
toggletogglebooleanfalseOpt the native child into APG toggle-button behavior.
selectedselectedbooleanNot specifiedControlled selected state. Host writes are silent.
defaultSelecteddefault-selectedbooleanNot specifiedInitial selected state for uncontrolled toggle usage.
iconOnlyicon-onlybooleanfalseIcon-only layout hint. May also be reflected by child classification.
fullWidthfull-widthbooleanfalseStretch the native child button to the host inline size.

Methods

No public methods are documented in the custom elements manifest.

Events

EventDetail typeDescription
rc-button-toggleCustomEventFired when a user activates a button with `toggle`.

Slots

NameDescription
(default)A direct native `<button>` child, or a direct `<a href>` for a navigation action (visual/layout styling only — see above).

CSS Custom Properties

PropertyDefaultDescription
--rc-button-gapNot specifiedGap between icon and label content.
--rc-button-block-sizeNot specifiedMinimum button block size.
--rc-button-min-inline-sizeNot specifiedMinimum button inline size.
--rc-button-inline-sizeNot specifiedButton inline size.
--rc-button-padding-blockNot specifiedButton block-axis padding (defers to native button padding when unset).
--rc-button-padding-inlineNot specifiedButton inline-axis padding (defers to native button padding when unset).
--rc-button-borderNot specifiedButton border (defers to native button border when unset).
--rc-button-radiusNot specifiedButton and overlay border radius (defers to native button radius when unset).
--rc-button-bgNot specifiedButton background (defers to native button background when unset).
--rc-button-colorNot specifiedButton text color (defers to native button color when unset).
--rc-button-shadowNot specifiedButton box shadow (defers to native button shadow when unset).
--rc-button-fontNot specifiedButton font shorthand (defers to native button font when unset).
--rc-button-transitionNot specifiedButton transition shorthand (defers to native button transition when unset).
--rc-button-icon-sizeNot specifiedButton inline and min-inline size when `icon-only`. Falls back to `--rc-button-block-size`, then `--rc-control-block-size`, then `2.5rem`.
--rc-button-touch-target-block-size3remMinimum accessible touch target block size for an `icon-only` button. A floor, not a fixed size: has no effect once `--rc-button-block-size` already meets or exceeds it. Grows the host (reserving layout space) and the light-DOM hit-slop (the actual larger clickable region) together; the visible child button itself stays at its own size, centered.
--rc-button-touch-target-inline-sizeNot specifiedMinimum accessible touch target inline size for an `icon-only` button. Defers to `--rc-button-touch-target-block-size` when unset.
--rc-button-touch-target-overlap-inline-start0pxZero by default. A theme or consumer sets this on an `icon-only` button that sits at a real leading edge (no neighbor on that side) to let its touch-target inflation overlap into whatever sits just outside the host, such as a container's own edge padding, instead of also reserving layout space there.
--rc-button-touch-target-overlap-inline-end0pxThe trailing-edge counterpart to `--rc-button-touch-target-overlap-inline-start`.
--rc-button-disabled-opacityNot specifiedDisabled button opacity (defers to native disabled styling when unset).
--rc-button-busy-content-colortransparentButton text color while `pending` or `progress`.
--rc-button-state-layer-bgcurrentColorState-layer overlay color.
--rc-button-state-layer-duration150msState-layer opacity transition duration.
--rc-button-state-layer-easingeaseState-layer opacity transition easing.
--rc-button-hover-state-layer-opacity0State-layer opacity on hover.
--rc-button-focus-state-layer-opacity0State-layer opacity on focus-within.
--rc-button-pressed-state-layer-opacity0State-layer opacity on active press.
--rc-button-progress-colorcurrentColorProgress affordance color.
--rc-button-progress-font600 0.75rem / 1 sans-serifDeterminate progress percentage font.
--rc-button-progress-size1.25remIndeterminate progress spinner diameter.
--rc-button-progress-track-width2pxIndeterminate progress spinner stroke width.
--rc-button-progress-track-colorcolor-mix(in srgb, currentColor 24%, transparent)Indeterminate progress spinner track color.
--rc-button-progress-active-colorcurrentColorIndeterminate progress spinner active arc color.

CSS Parts

PartDescription
state-layerOverlay layer for hover, focus, pressed, ripple, or design-system effects.
progressNon-interactive progress affordance overlay shown for `pending` or `progress`.