React Clean UI
Form

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

Loom Previewv0.13.2

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.

Loom Previewv0.13.2

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)}
/>
  • onRelease fires 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.
  • disabled blocks press-down activation. A press that began while the button was enabled still gets its onRelease if the button is disabled mid-hold.
  • Gamepad ButtonA and keyboard activation of a selected button still go through the native Activated at the normal time, and never get an onRelease.
  • 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

PropTypeDefaultDescription
textstringundefinedRenders a label using the button's typography and intent color.
iconIconNameundefinedRenders a leading icon using the button's intent color.
fontWeightEnum.FontWeightundefinedOverrides the font weight used by the label.
childrenReact.ReactNodeundefinedCustom content rendered after the automatic icon/label row.
groupbooleanfalseMatches this button's width to the widest button in its Group.
disabledbooleanfalseDisables hover/click interaction and switches to disabled colors.
modalbooleanfalseSets the native Modal property, freeing the mouse cursor while the button is visible (even in first-person or shift-lock).
widthResponsiveCssSizeundefinedSets the button's width (px, "50%", or a breakpoint table); the height still fits the content. Wins over a Group's pinned width.
fillbooleanfalseAdds 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) => voidundefinedWith pressOn="down", called when a mouse/touch press that activated the button ends or is cancelled.
LayoutOrdernumberundefinedNative Roblox layout order.
paddingCssPaddingundefinedOverrides the spacing-derived internal padding, per side (for example 8, "4 12", or "4px 12px 4px 12px").
styleOverrideButtonStyleOverrideundefinedOverrides theme values (colors, corners, border, etc.) for this instance only.
EventReact.InstanceEvent<ImageButton>undefinedNative Roblox event handlers, including Activated.

Shared props

The button also supports props inherited from the following interfaces.

InterfacePurpose
IntentElementPropsConfigures the button's base intent.
IconElementPropsConfigures the icon prop's accepted values.
BackgroundElementPropsConfigures background colour and transparency.
SpacedElementPropsConfigures theme-based internal spacing.
ScalableElementPropsConfigures the label and icon scale.
ShadowElementPropsConfigures a box shadow.
ZIndexElementPropsConfigures the button's ZIndex.

Behaviour

  • Without width or fill, the button automatically grows to fit its content (AutomaticSize.XY). With width or fill, 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 hover state.
  • Setting disabled sets Active={false}, ignores hover and Activated events, and switches colours to the intent's disabled state (when defined in the theme).
  • When group is set and a Group ancestor is present, the button's width is fixed to the widest button reporting into that Group, unless width is set; its height still grows automatically. With group and fill but no width, the group's width is the base the button grows from.
  • With pressOn="down", your own Event.InputBegan handler still runs, before the press-down activation. Hover behaviour is the same in both modes.
  • Without text, icon, or children, the button renders with no content.
  • modal is passed straight through to the ImageButton's native Modal property; when unset, Roblox's default (false) applies.
  • Internal padding comes from spacing (against theme.spacing, with "None" meaning 0). When padding is set, its parsed per-side values replace the spacing-derived ones. The resolved padding is also reported to Group, so a grouped button's width includes it.
  • Background transparency resolves as BackgroundTransparency prop, then styleOverride.backgroundTransparency, then the current intent's own backgroundTransparency (non-primary intents only), then theme.components.button.backgroundTransparency.
  • Border thickness resolves as the intent state's borderThickness, then styleOverride.borderThickness, then theme.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, and disabled states, plus optional backgroundImage and backgroundGradient

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 than primary, overrides theme.components.button.backgroundTransparency. It is read from that intent's current state, then that intent's default entry, and is never inherited from primary. For primary, the component-level backgroundTransparency always applies.

These values can be changed by providing a custom Clean UI theme.

GitHub Repository

On this page