Primitives
Button
Accessible action button with size, variant, and semantic tone support.
Composition
Action button
- <Button>
- button content
Navigation link
- <ButtonLink>
- link content
Label-backed control
- <ButtonLabel>
- label content
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/button.tsx.
npx shadcn@latest add https://control-ui.dev/r/button.jsonDependencies
Control variants
src/registry/sources/control-ui/control-variants.tsSupportRaw code
Primary installed source
"use client";import { Button as BaseButton } from "@base-ui/react/button";import { useRender } from "@base-ui/react/use-render";import type { ComponentProps, CSSProperties, ReactNode } from "react";import { Children, createContext, Fragment, isValidElement, useContext } from "react";import type { RenderProp } from "@/components/control-ui/control-props";import type { ControlSize, ControlTone, ControlVariant } from "@/components/control-ui/control-variants";import type { ButtonKnobStyle } from "@/components/control-ui/knob-contracts/button-knobs";import { cn } from "@/components/control-ui/lib/cn";import { skinAdornment } from "@/components/control-ui/skin";import { useSkin } from "@/components/control-ui/skin-provider";export type ButtonVariant = ControlVariant;export type ButtonSize = ControlSize;export type ButtonTone = ControlTone;export const ButtonTrackContext = createContext(false);export const buttonShapes = ["default", "circle"] as const;export type ButtonShape = (typeof buttonShapes)[number];export const buttonLayouts = ["inline", "tile"] as const;export type ButtonLayout = (typeof buttonLayouts)[number];export type ButtonAppearanceProps = { variant?: ButtonVariant; size?: ButtonSize; tone?: ButtonTone; active?: boolean; iconOnly?: boolean; shape?: ButtonShape; layout?: ButtonLayout; style?: CSSProperties & ButtonKnobStyle;};export type ButtonProps = ComponentProps<"button"> & ButtonAppearanceProps & { render?: RenderProp<ComponentProps<"button">, { disabled: boolean }>; nativeButton?: boolean; focusableWhenDisabled?: boolean; };export type ButtonLinkProps = ComponentProps<"a"> & ButtonAppearanceProps & { render?: RenderProp<ComponentProps<"a">>; };export type ButtonLabelProps = ComponentProps<"label"> & ButtonAppearanceProps;export const buttonStructureClasses = "isolate inline-flex shrink-0 items-center justify-center overflow-visible whitespace-nowrap";export const buttonTrackStructureClasses = "data-[track=hover]:relative data-[track=hover]:isolate [&_[data-track-item]]:[isolation:auto]";export const buttonContentClasses = "relative z-[1] inline-flex min-w-0 items-center justify-center gap-[inherit]";function wrapButtonText(children: ReactNode): ReactNode { return Children.map(children, (child) => { if (typeof child === "string" || typeof child === "number") { return <span className="contents">{child}</span>; } if (isValidElement<{ children?: ReactNode }>(child) && child.type === Fragment) { return <Fragment>{wrapButtonText(child.props.children)}</Fragment>; } return child; });}function ButtonContent({ children }: { children: ReactNode }) { return ( <span data-control-ui="button" data-control-family="button" data-slot="content" className={buttonContentClasses}> {children} </span> );}function ButtonBody({ layer, wrap = true, children }: { layer: ReactNode; wrap?: boolean; children: ReactNode }) { const tracksHover = useContext(ButtonTrackContext); const wrapsContent = tracksHover || (wrap && Boolean(layer)); const content = wrapButtonText(children); return ( <> {layer} {wrapsContent ? <ButtonContent>{content}</ButtonContent> : content} </> );}export function Button({ variant = "quiet", size = "sm", tone = "neutral", active = false, iconOnly = false, shape = "default", layout = "inline", type = "button", disabled, render, nativeButton, className, children, ...props}: ButtonProps) { const skin = useSkin(); const layer = skinAdornment(skin, "button", "layer", { variant, tone }); const tracksHover = useContext(ButtonTrackContext); return ( <BaseButton type={nativeButton === false ? undefined : type} disabled={disabled} data-control-ui="button" data-control-family="button" data-slot="root" data-control="true" data-track-item={tracksHover ? "" : undefined} data-active={active ? "true" : undefined} data-icon-only={iconOnly ? "true" : undefined} data-shape={shape} data-layout={layout} data-variant={variant} data-tone={tone} data-size={size} className={cn(buttonStructureClasses, className)} render={render} nativeButton={nativeButton} {...props} > <ButtonBody layer={layer} wrap={!render}> {children} </ButtonBody> </BaseButton> );}export function ButtonLink({ variant = "quiet", size = "sm", tone = "neutral", active = false, iconOnly = false, shape = "default", layout = "inline", render, className, children, ...props}: ButtonLinkProps) { const skin = useSkin(); const tracksHover = useContext(ButtonTrackContext); return useRender({ defaultTagName: "a", render, props: { "data-track-item": tracksHover ? "" : undefined, ...props, "data-control-ui": "button", "data-control-family": "button", "data-slot": "root", "data-control": "true", "data-active": active ? "true" : undefined, "data-icon-only": iconOnly ? "true" : undefined, "data-shape": shape, "data-layout": layout, "data-variant": variant, "data-tone": tone, "data-size": size, className: cn(buttonStructureClasses, className), children: <ButtonBody layer={skinAdornment(skin, "button", "layer", { variant, tone })}>{children}</ButtonBody>, }, });}export function ButtonLabel({ variant = "quiet", size = "sm", tone = "neutral", active = false, iconOnly = false, shape = "default", layout = "inline", className, children, ...props}: ButtonLabelProps) { const skin = useSkin(); const tracksHover = useContext(ButtonTrackContext); return ( // biome-ignore lint/a11y/noLabelWithoutControl: The wrapped file input is supplied through children. <label data-track-item={tracksHover ? "" : undefined} {...props} data-control-ui="button" data-control-family="button" data-slot="root" data-control="true" data-active={active ? "true" : undefined} data-icon-only={iconOnly ? "true" : undefined} data-shape={shape} data-layout={layout} data-variant={variant} data-tone={tone} data-size={size} className={cn(buttonStructureClasses, className)} > <ButtonBody layer={skinAdornment(skin, "button", "layer", { variant, tone })}>{children}</ButtonBody> </label> );}Available extensions
Optional, separately installed items this surface can host — not part of the component’s bundle.
ControlEffectsRoot-mounted
CSS-driven control effects (top-shine, ripple, hover-circle) that follow every control app-wide through the emitted anatomy — portalled surfaces included.
Effects
npx shadcn@latest add https://control-ui.dev/r/control-effects.json// skin.config.tsx — a brand's controls either all ripple or none doexport const skin: ControlUiSkin = { id: "my-brand", effects: ["ripple"],};// app layout — mirrors resolved list on <html> for in-tree controls<ControlEffectsRuntime />// or scope effects to one subtree instead (caller-wins local override)<ControlEffectsRoot effects={["top-shine", "ripple"]}> <Toolbar /></ControlEffectsRoot>Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-button-* · 25 knobsHow the cascade resolves--cui-button-radius<length-percentage>var(--nest-radius, var(--radius-control))--cui-button-gap<length-percentage>calc(var(--spacing) * 1.5)--cui-button-icon<length>1rem--cui-button-height<length-percentage>var(--control-h-md)--cui-button-padding-inline<length-percentage>var(--padding-x)--cui-button-icon-padding-inline<length-percentage>max(0px, calc(var(--cui-button-padding-inline) - var(--spacing) * 0.5))--cui-button-font-size<length-percentage>var(--text-body)--cui-button-background<color>transparent--cui-button-background-image*none--cui-button-hover-background-image*var(--cui-button-background-image)--cui-button-foreground<color>var(--muted-foreground)--cui-button-hover-background<color>var(--hover-fill)--cui-button-hover-foreground<color>var(--foreground)--cui-button-press-background<color>var(--cui-button-hover-background)--cui-button-press-scale<number>0.96--cui-button-active-background<color>var(--active-fill)--cui-button-active-foreground<color>var(--foreground)--cui-button-active-hover-background<color>var(--cui-button-active-background)--cui-button-shadow*0 0 transparent--cui-button-hover-shadow*var(--cui-button-shadow)--cui-button-press-shadow*var(--cui-button-hover-shadow)--cui-button-active-shadow*var(--cui-button-shadow)--cui-button-open-background<color>var(--cui-button-hover-background)--cui-button-open-foreground<color>var(--cui-button-hover-foreground)--cui-button-open-shadow*var(--cui-button-shadow)