ButtonBase — spec¶
Synthesized from MUI
ButtonBase, with public action-button guidance cross-checked against Ant DesignButtonand shadcn-uiButton.ButtonBaseis a low-level interactive primitive for building design-system controls, not a product-facing component.
When to use¶
- Building a new design-system primitive such as
IconButton,CardActionArea,ListItemButton,Tab,StepButton, or a toolbar control. - You need native button/link semantics, focus-visible handling, disabled behavior, and optional press/ripple feedback without inheriting a visual Button style.
- You are authoring a library layer where the consumer owns the visual treatment.
When NOT to use:
- Product actions such as "저장", "결제하기", or "삭제" — use Button.
- Icon-only actions — use IconButton so the accessible name, size, and variant contract are already enforced.
- Links inside body copy — use Link.
- Toggle formatting controls — use Toggle / ToggleGroup.
Anatomy¶
ButtonBase
├── Root interactive element (`button` by default)
├── Focus-visible state bridge
├── Optional TouchRipple layer
└── Children
| Part | Purpose | Required | Default if omitted |
|---|---|---|---|
| Root | Receives events, disabled state, tabIndex, and semantic element choice. |
yes | <button type="button"> |
| Focus-visible bridge | Applies keyboard-only focus state and exposes onFocusVisible. |
yes | internal state/class |
| TouchRipple | Optional visual feedback layer for pointer/keyboard press. | no | rendered unless ripple is disabled |
| Children | The visual content supplied by the composed component. | no | empty interactive shell; avoid in product code |
API¶
<ButtonBase
component="button"
type="button"
focusVisibleClassName="focus-visible"
onFocusVisible={handleFocusVisible}
>
<span className="toolbar-button-content">정렬</span>
</ButtonBase>
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
— | Visual content. The composed component must provide text or an accessible label. |
component |
React.ElementType |
"button" |
Root element override. Use sparingly and preserve keyboard behavior. |
href |
string |
— | Switches the root toward link behavior in MUI's overload. |
LinkComponent |
React.ElementType |
"a" |
Router-aware link component when href / to is present. |
type |
string |
"button" |
Native button type. Keep the default unless submitting a form intentionally. |
disabled |
boolean |
false |
Removes interaction. Non-native roots still need aria-disabled handling. |
tabIndex |
number |
0 |
Keyboard order override. Avoid positive values. |
focusVisibleClassName |
string |
— | Class applied for keyboard focus-visible styling. |
onFocusVisible |
(event) => void |
— | Fires when focus was reached through keyboard-like interaction. |
action |
ref |
— | Imperative handle; supports focusVisible(). |
centerRipple |
boolean |
false |
Centers ripple instead of starting from pointer location. |
focusRipple |
boolean |
false |
Adds keyboard-focus ripple feedback. |
disableRipple |
boolean |
false |
Removes ripple. Must be paired with an explicit focus-visible visual. |
disableTouchRipple |
boolean |
false |
Removes touch ripple while keeping other focus behavior. |
TouchRippleProps |
object |
— | Props passed to the ripple layer. Treat as advanced. |
touchRippleRef |
ref |
— | Imperative access to ripple actions. Treat as internal. |
nativeButton |
boolean |
inferred | Declares whether a custom component renders a real <button>. |
sx / classes |
system styles | — | MUI-specific style extension points. Prefer local component tokens in this repo. |
API choices made¶
- Keep
ButtonBaseadvanced/internal. Product teams should rarely import it directly because it has no visible affordance by itself. - Preserve MUI's polymorphic
component/LinkComponentmodel, but document it as a semantic responsibility: if the root is not a native button or anchor, the wrapper must recreate keyboard activation and ARIA state. - Do not expose visual variants here. Variants belong to composed controls (
Button,IconButton,Toggle) so the primitive stays stable. - Treat ripple as optional decoration. Focus-visible is mandatory whether ripple is on or off.
States¶
| State | Trigger | Visual / behavior |
|---|---|---|
| Default | resting | Transparent reset; composed component owns size, color, and layout. |
| Hover | pointer hover | Optional consumer style using --color-bg-action-hover; not defined by the base. |
| Focus-visible | keyboard focus | 2px outline using --color-focus-ring, offset 2px, contrast at least 3:1 against adjacent surfaces. |
| Active | pointer or keyboard press | Optional pressed layer or ripple; do not rely on motion alone. |
| Disabled | disabled |
Remove pointer activation, suppress ripple, expose disabled semantics. |
| Link mode | href / to |
Behaves as navigation; do not attach form-submit semantics. |
Tokens consumed¶
ButtonBase itself should consume only primitive interaction tokens. Composed controls map these to real visual tokens.
--color-focus-ring
--color-bg-action-hover
--color-bg-action-active
--color-text-primary
--space-1
--space-2
--radius-sm
--motion-fast
--easing-out
Accessibility¶
- Semantic element: default to
<button type="button">; use<a>only for navigation. - Keyboard:
Tabreaches the control,EnterandSpaceactivate button semantics. Custom roots must implement both keys. - Focus: visible focus is non-negotiable. If
disableRippleis true, add a separate focus-visible style; otherwise keyboard users lose the only visible cue. - Disabled: native
disabledis enough for real buttons; custom roots needaria-disabled="true", suppressed events, and removal from activation shortcuts. - Touch target: composed controls must provide at least 44x44 mobile hit area and at least 24x24 WCAG AA web target size.
- Name: icon-only composition requires
aria-label; decorative icons inside a text label needaria-hidden="true". - Toggle state: expose
aria-pressedonly for two-state buttons. Do not putaria-selectedon generic buttons.
Code example — design-system toolbar button¶
type ToolbarButtonProps = {
label: string;
selected?: boolean;
disabled?: boolean;
onPress: () => void;
children: React.ReactNode;
};
export function ToolbarButton({
label,
selected = false,
disabled = false,
onPress,
children,
}: ToolbarButtonProps) {
return (
<ButtonBase
type="button"
disabled={disabled}
aria-label={label}
aria-pressed={selected}
focusVisibleClassName="toolbar-button-focus-visible"
className="toolbar-button"
onClick={onPress}
>
{children}
</ButtonBase>
);
}
.toolbar-button {
min-width: 44px;
min-height: 44px;
border-radius: var(--radius-sm);
color: var(--color-text-primary);
}
.toolbar-button:hover {
background: var(--color-bg-action-hover);
}
.toolbar-button[aria-pressed="true"] {
background: var(--color-bg-action-active);
}
.toolbar-button-focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
Edge cases¶
- Inside a form: omit
typeand a native button may submit unexpectedly in some wrappers. Keeptype="button"for non-submit actions. - Router links: pass a router-aware
LinkComponent, but keep anchor semantics when the action navigates. - Disabled links: anchors do not support native
disabled; usearia-disabled, removehrefor intercept activation, and keep the visual disabled state. - Nested interactive children: never put another button, input, select, or link inside
ButtonBase. - No ripple: disabling ripple is fine for quiet UIs, but the focus-visible ring must still be explicit.
- Korean dense toolbars: reduce visual padding if needed, but keep the hit target through invisible padding or
::beforehit area.
Don't¶
- Don't import
ButtonBasein product pages whenButton,IconButton, orLinkfits. - Don't render a
<div>root unless you also implement role, tab stop, disabled suppression, and keyboard activation. - Don't remove focus outlines globally to make ripple the only state indicator.
- Don't use ripple or pressed animation as the only selected-state signal.
References¶
- MUI:
ButtonBase.d.ts,ButtonBase.js,useButtonBase.ts - Ant Design:
Button.tsx,button/index.en-US.md - shadcn-ui:
button.tsx - Knowledge:
a11y/keyboard-and-focus.md,layout/spacing-and-grid.md - Cross-reference:
component-button.md,component-icon-button.md,component-toggle.md