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- Related
- Theme previews
Installation
- npm
- Yarn
npm install @rcarls/rc-button
yarn add @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
| Property | Markup | Type | Default | Description |
|---|---|---|---|---|
disabled | disabled | boolean | false | Mirror disabled state to the native child. |
pending | pending | boolean | false | Show an indeterminate progress affordance and disable the native button. |
progress | progress | boolean | false | Show a progress affordance and disable the native button. |
progressValue | progress-value | number | undefined | Not specified | Optional determinate progress percentage, clamped from 0 through 100. |
toggle | toggle | boolean | false | Opt the native child into APG toggle-button behavior. |
selected | selected | boolean | Not specified | Controlled selected state. Host writes are silent. |
defaultSelected | default-selected | boolean | Not specified | Initial selected state for uncontrolled toggle usage. |
iconOnly | icon-only | boolean | false | Icon-only layout hint. May also be reflected by child classification. |
fullWidth | full-width | boolean | false | Stretch the native child button to the host inline size. |
Methods
No public methods are documented in the custom elements manifest.
Events
| Event | Detail type | Description |
|---|---|---|
rc-button-toggle | CustomEvent | Fired when a user activates a button with `toggle`. |
Slots
| Name | Description |
|---|---|
(default) | A direct native `<button>` child, or a direct `<a href>` for a navigation action (visual/layout styling only — see above). |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--rc-button-gap | Not specified | Gap between icon and label content. |
--rc-button-block-size | Not specified | Minimum button block size. |
--rc-button-min-inline-size | Not specified | Minimum button inline size. |
--rc-button-inline-size | Not specified | Button inline size. |
--rc-button-padding-block | Not specified | Button block-axis padding (defers to native button padding when unset). |
--rc-button-padding-inline | Not specified | Button inline-axis padding (defers to native button padding when unset). |
--rc-button-border | Not specified | Button border (defers to native button border when unset). |
--rc-button-radius | Not specified | Button and overlay border radius (defers to native button radius when unset). |
--rc-button-bg | Not specified | Button background (defers to native button background when unset). |
--rc-button-color | Not specified | Button text color (defers to native button color when unset). |
--rc-button-shadow | Not specified | Button box shadow (defers to native button shadow when unset). |
--rc-button-font | Not specified | Button font shorthand (defers to native button font when unset). |
--rc-button-transition | Not specified | Button transition shorthand (defers to native button transition when unset). |
--rc-button-icon-size | Not specified | Button 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-size | 3rem | Minimum 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-size | Not specified | Minimum 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-start | 0px | Zero 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-end | 0px | The trailing-edge counterpart to `--rc-button-touch-target-overlap-inline-start`. |
--rc-button-disabled-opacity | Not specified | Disabled button opacity (defers to native disabled styling when unset). |
--rc-button-busy-content-color | transparent | Button text color while `pending` or `progress`. |
--rc-button-state-layer-bg | currentColor | State-layer overlay color. |
--rc-button-state-layer-duration | 150ms | State-layer opacity transition duration. |
--rc-button-state-layer-easing | ease | State-layer opacity transition easing. |
--rc-button-hover-state-layer-opacity | 0 | State-layer opacity on hover. |
--rc-button-focus-state-layer-opacity | 0 | State-layer opacity on focus-within. |
--rc-button-pressed-state-layer-opacity | 0 | State-layer opacity on active press. |
--rc-button-progress-color | currentColor | Progress affordance color. |
--rc-button-progress-font | 600 0.75rem / 1 sans-serif | Determinate progress percentage font. |
--rc-button-progress-size | 1.25rem | Indeterminate progress spinner diameter. |
--rc-button-progress-track-width | 2px | Indeterminate progress spinner stroke width. |
--rc-button-progress-track-color | color-mix(in srgb, currentColor 24%, transparent) | Indeterminate progress spinner track color. |
--rc-button-progress-active-color | currentColor | Indeterminate progress spinner active arc color. |
CSS Parts
| Part | Description |
|---|---|
state-layer | Overlay layer for hover, focus, pressed, ripple, or design-system effects. |
progress | Non-interactive progress affordance overlay shown for `pending` or `progress`. |