Button
A clickable button with icons, intents, scaling, and grouped children.
Button is a themed, clickable surface built on Roblox's native ImageButton. It automatically applies the current Clean UI theme, including background, border, corner radius, and hover styling based on intent.
Import
import { Button } from "@rbxts/react-clean-ui";Basic usage
The text prop renders a label, and icon renders a leading icon. Either can be used on its own.
<Button text="Save" />
<Button icon="save" />Responding to clicks
Button renders an ImageButton, so click handling is done through the native Event prop.
<Button
text="Submit"
intent="primary"
Event={{
Activated: () => {
print("Submitted");
},
}}
/>Intents
Set intent to control the button's background, border, and text/icon colors.
<Button text="Primary" intent="primary" />
<Button text="Secondary" intent="secondary" />
<Button text="Success" intent="success" />
<Button text="Info" intent="info" />
<Button text="Warning" intent="warning" />
<Button text="Danger" intent="danger" />Hovering a button switches its colors to the intent's hover state.
secondary is a lower-emphasis alternative to primary, such as a neutral grey button in the default theme. Every shipped theme styles it. A custom theme that doesn't define secondary still works: the button falls back to the primary colors.
Custom content
For layouts beyond an icon and a label, pass children instead of icon/text. Button.Icon and Button.Text are exposed so custom content can still pick up the button's intent and scale.
<Button intent="info">
<HStack valign="Center" spacing="sm">
<Button.Icon icon="download" intent="info" />
<Button.Text text="Download report" intent="info" />
</HStack>
</Button>When both icon/text and children are provided, the automatic icon/label row renders first, followed by children.
Scaling
Button supports the shared scale prop, which resolves the button's typography and icon size from the active theme.
<Button text="Small" scale="sm" />
<Button text="Large" scale="lg" />Available scales are xs, sm, md, lg, and xl.
Spacing
The shared spacing prop controls the gap between the icon and text, as well as the button's internal padding.
<Button icon="star" text="Spacious" spacing="lg" />Padding
Set padding to override the button's internal padding with a CSS-like value. Like CSS, it accepts one value for all sides, two values (vertical, horizontal), three values (top, horizontal, bottom), or four values (top, right, bottom, left), with or without px. A single number is also accepted.
<Button text="Even" padding={8} />
<Button text="Wide" padding="4 12" />
<Button text="Custom" padding="4px 12px 4px 12px" />padding overrides the padding derived from spacing side by side, including when spacing="None". The gap between the icon and text still follows spacing.
Disabled state
Set disabled to prevent interaction. The button switches to the intent's disabled colors (when defined in the theme) and ignores hover and click events.
<Button text="Disabled" intent="primary" disabled />Freeing the mouse cursor
Set modal to forward Roblox's native Modal property to the underlying ImageButton. While the button is visible, Roblox frees the mouse cursor even in first-person or shift-lock, so players can click menu buttons while the camera is locked.
<Button text="Menu" modal />This is unrelated to Clean UI's own Modal component and modal provider. It only controls the native cursor behaviour.
Grouped buttons
Set group on a Button and wrap the group in a Group to make every grouped button match the width of the widest one. This keeps a stack of buttons with icons or labels of differing lengths visually aligned.
import { Button, Group, VStack } from "@rbxts/react-clean-ui";
<Group>
<VStack>
<Button group text="Save" intent="primary" />
<Button group text="Save and continue" intent="primary" />
<Button group icon="times" text="Cancel" />
</VStack>
</Group>Sizing
Without width or fill, a button grows to fit its content. Use width for a fixed or relative width, and fill to have the button take the free space in its parent's layout. In both cases the icon and label are centred inside the button.
Width
width accepts the same values as Container's width: a pixel number, "120px", a percentage such as "50%", or a breakpoint table. The height still fits the content. width="Auto" keeps the default fit-to-content sizing.
<Button text="Fixed" width={120} />
<Button text="Half" width="50%" />width wins over a Group's pinned width, so a grouped button with width set keeps its own width.
Fill
fill adds a UIFlexItem with FlexMode set to Fill, so the button grows (and may shrink) along its parent UIListLayout to take the free space. Without width, the button's base width is 0, so several fill buttons in one row share it equally whatever their label lengths. With width, that width is the base the button grows from.
<HStack>
<Button fill text="Save" />
<Button fill text="Save and continue" />
</HStack>fill only works when the button is a direct child of a frame with a UIListLayout, such as HStack, Row, or VStack. Inside a VStack it grows vertically, along that stack's direction. Elsewhere, a fill button without width collapses to its padding width.
The icon and label row never wraps, so a fill or width button narrower than its content overflows rather than wrapping.
Press-down activation
By default a mouse or touch press activates the button on release, through the native Activated event. Set pressOn="down" to activate it as soon as the press begins instead. Your Event.Activated handler is called on InputBegan for MouseButton1 and Touch, and the native Activated that follows for the same press is swallowed, so each press fires once.
Pair it with onRelease to know when the press ends, for example for a hold-to-sprint button:
<Button
text="Sprint"
pressOn="down"
Event={{
Activated: () => setSprinting(true),
}}
onRelease={() => setSprinting(false)}
/>onReleasefires once per press-down activation, when that mouse or touch input ends or is cancelled, even if the pointer was dragged off the button.- Each touch is tracked separately, so two fingers on one button give two activations and two releases.
disabledblocks press-down activation. A press that began while the button was enabled still gets itsonReleaseif the button is disabled mid-hold.- Gamepad
ButtonAand keyboard activation of a selected button still go through the nativeActivatedat the normal time, and never get anonRelease. - Presses still held when the button unmounts are dropped without calling
onRelease.
Because gamepad presses have no release, don't rely on onRelease for gamepad hold actions. For a gamepad hold-to-sprint, bind the action separately (for example with ContextActionService:BindAction), which reports both the start and the end of the press.
Props
Button-specific props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | undefined | Renders a label using the button's typography and intent color. |
icon | IconName | undefined | Renders a leading icon using the button's intent color. |
fontWeight | Enum.FontWeight | undefined | Overrides the font weight used by the label. |
children | React.ReactNode | undefined | Custom content rendered after the automatic icon/label row. |
group | boolean | false | Matches this button's width to the widest button in its Group. |
disabled | boolean | false | Disables hover/click interaction and switches to disabled colors. |
modal | boolean | false | Sets the native Modal property, freeing the mouse cursor while the button is visible (even in first-person or shift-lock). |
width | ResponsiveCssSize | undefined | Sets the button's width (px, "50%", or a breakpoint table); the height still fits the content. Wins over a Group's pinned width. |
fill | boolean | false | Adds a UIFlexItem (Fill) so the button takes the free space along its parent UIListLayout. |
pressOn | "down" | "up" | "up" | When a mouse/touch press activates the button: on release ("up") or as soon as it begins ("down"). |
onRelease | (button: ImageButton, input: InputObject) => void | undefined | With pressOn="down", called when a mouse/touch press that activated the button ends or is cancelled. |
LayoutOrder | number | undefined | Native Roblox layout order. |
padding | CssPadding | undefined | Overrides the spacing-derived internal padding, per side (for example 8, "4 12", or "4px 12px 4px 12px"). |
styleOverride | ButtonStyleOverride | undefined | Overrides theme values (colors, corners, border, etc.) for this instance only. |
Event | React.InstanceEvent<ImageButton> | undefined | Native Roblox event handlers, including Activated. |
Shared props
The button also supports props inherited from the following interfaces.
| Interface | Purpose |
|---|---|
IntentElementProps | Configures the button's base intent. |
IconElementProps | Configures the icon prop's accepted values. |
BackgroundElementProps | Configures background colour and transparency. |
SpacedElementProps | Configures theme-based internal spacing. |
ScalableElementProps | Configures the label and icon scale. |
ShadowElementProps | Configures a box shadow. |
ZIndexElementProps | Configures the button's ZIndex. |
Behaviour
- Without
widthorfill, the button automatically grows to fit its content (AutomaticSize.XY). Withwidthorfill, only the height is automatic, and the icon/label row is centred horizontally. - Hovering the button switches its background, border, and text/icon colours to the intent's
hoverstate. - Setting
disabledsetsActive={false}, ignores hover andActivatedevents, and switches colours to the intent'sdisabledstate (when defined in the theme). - When
groupis set and aGroupancestor is present, the button's width is fixed to the widest button reporting into thatGroup, unlesswidthis set; its height still grows automatically. Withgroupandfillbut nowidth, the group's width is the base the button grows from. - With
pressOn="down", your ownEvent.InputBeganhandler still runs, before the press-down activation. Hover behaviour is the same in both modes. - Without
text,icon, orchildren, the button renders with no content. modalis passed straight through to theImageButton's nativeModalproperty; when unset, Roblox's default (false) applies.- Internal padding comes from
spacing(againsttheme.spacing, with"None"meaning0). Whenpaddingis set, its parsed per-side values replace the spacing-derived ones. The resolved padding is also reported toGroup, so a grouped button's width includes it. - Background transparency resolves as
BackgroundTransparencyprop, thenstyleOverride.backgroundTransparency, then the current intent's ownbackgroundTransparency(non-primaryintents only), thentheme.components.button.backgroundTransparency. - Border thickness resolves as the intent state's
borderThickness, thenstyleOverride.borderThickness, thentheme.components.button.borderThickness.
Theme values
The button uses values from theme.components.button for:
- Corner radius
- Border thickness
- Background transparency
- Typography
- Box shadow
- Per-intent background, border, and text/icon colours for the
default,hover, anddisabledstates, plus optionalbackgroundImageandbackgroundGradient
Each intent state in theme.components.button.intents can also set:
borderThickness— overrides the button's border thickness for that intent and state.backgroundTransparency— for any intent other thanprimary, overridestheme.components.button.backgroundTransparency. It is read from that intent's current state, then that intent'sdefaultentry, and is never inherited fromprimary. Forprimary, the component-levelbackgroundTransparencyalways applies.
These values can be changed by providing a custom Clean UI theme.