A button that can render as another tag and stay focusable when disabled. The Button component extends Base UI's Button. It adds variant, size, and isLoading props, with icon variants for icon-only buttons.
Here is the component implementation code that you can copy to your project:
// https://devie-ui.com/components/button
// https://base-ui.com/react/components/button
import { Button as BaseButton } from "@base-ui/react/button";
import clsx from "clsx";
import styles from "./Button.module.scss";
type Variant =
| "primary"
| "secondary"
| "danger"
| "naked"
| "icon-primary"
| "icon-secondary"
| "icon-danger"
| "icon-naked";
type Size = "sm" | "md" | "xl";
function Button({
variant = "primary",
size = "md",
children,
className,
disabled,
isLoading,
...props
}: Button.Props) {
return (
<BaseButton
className={clsx(
styles.button,
variant === "primary" && styles.variantPrimary,
variant === "secondary" && styles.variantSecondary,
variant === "danger" && styles.variantDanger,
variant === "naked" && styles.variantNaked,
variant === "icon-primary" && [styles.variantPrimary, styles.icon],
variant === "icon-secondary" && [styles.variantSecondary, styles.icon],
variant === "icon-danger" && [styles.variantDanger, styles.icon],
variant === "icon-naked" && [styles.variantNaked, styles.icon],
size === "sm" && styles.sizeSm,
size === "md" && styles.sizeMd,
size === "xl" && styles.sizeXl,
className,
)}
data-loading={isLoading}
disabled={disabled || isLoading}
focusableWhenDisabled={isLoading}
{...props}
>
{children}
{isLoading && (
<div className={styles.loadingOverlay}>
<div className={styles.loader} />
</div>
)}
</BaseButton>
);
}
namespace Button {
export interface Props extends BaseButton.Props {
variant?: Variant;
size?: Size;
isLoading?: boolean;
}
export type State = BaseButton.State;
}
export default Button;Set variant to primary, secondary, danger, or naked. Use danger for a destructive action and naked for a button without background.
Set size to sm, md, or xl. The default is md.
Set isLoading to show a spinner. The button keeps its width, so the layout does not shift.
Set disabled to prevent interaction.
Set variant to icon-primary, icon-secondary, icon-danger, or icon-naked and give the button an aria-label. With an icon up to 16px, it stays square at the height of a labeled button of the same size.