React Clean UI
Form

HoverButton

A low-level button primitive that swaps its props between default, hover, and selected states.

HoverButton is an unthemed primitive built on Roblox's native ImageButton. You give it one set of ImageButton properties for each state (default, hover, and focus), and it renders whichever set matches the current hover or selected state. Clean UI uses it inside interactive controls such as Accordion headers and Tabs triggers. Use it when you need full control over a button's appearance. For a ready-styled button, use Button.

Import

import { HoverButton } from "@rbxts/react-clean-ui";

Basic usage

Loom Previewv0.13.2

default is required and always applies. hover and focus are optional and are layered on top of default, so each one only needs the properties that change in that state.

States

The active state is chosen in this order:

  1. focus while isSelected is true, wherever the pointer is.
  2. hover while the pointer is over the button.
  3. default alone otherwise.

The rendered props are { ...default, ...activeState }.

<HoverButton
    isSelected={selected}
    default={{
        Size: UDim2.fromOffset(120, 40),
        BackgroundColor3: Color3.fromHex("#F4F5F7"),
        BackgroundTransparency: 0,
        AutoButtonColor: false,
        Event: {
            Activated: () => setSelected(!selected),
        },
    }}
    hover={{ BackgroundColor3: Color3.fromHex("#E1E4E8") }}
    focus={{ BackgroundColor3: Color3.fromHex("#2E9D63") }}
/>

Set AutoButtonColor: false in default. Otherwise Roblox's built-in hover and press tinting is applied on top of your own hover and focus styling.

Borders

Set borderThickness to draw a UIStroke around the button. A stroke is drawn only when the resolved thickness is greater than 0. With no border props anywhere, no stroke is rendered.

You can set the border props on HoverButton itself, inside default, hover, or focus, or in more than one place. Each field is resolved separately, and the state value wins over the top-level prop. So a hover set can change just the borderColor and keep the top-level thickness:

<HoverButton
    borderThickness={2}
    borderPosition="inner"
    default={{
        Size: UDim2.fromScale(1, 1),
        BackgroundColor3: Color3.fromHex("#F4F5F7"),
        BackgroundTransparency: 0,
        AutoButtonColor: false,
        borderColor: Color3.fromHex("#8A8F98"),
    }}
    hover={{ borderColor: Color3.fromHex("#4A7AC9") }}
    focus={{
        borderColor: Color3.fromHex("#2E9D63"),
        borderThickness: 3,
    }}
/>

borderPosition defaults to "inner", which keeps the stroke inside the button's bounds. Use "outer" to draw it outside them.

Reacting to state in children

HoverButton gives its children a HoverButtonContext holding { hover: boolean; isSelected: boolean }. Nested content can read the button's current state from it without tracking hover itself.

import React, { useContext } from "@rbxts/react";
import { HoverButtonContext, Text } from "@rbxts/react-clean-ui";

function Label() {
    const state = useContext(HoverButtonContext);

    return <Text text={state?.hover ? "Hovered" : "Idle"} />;
}

Press-down activation

HoverButton supports the same pressOn and onRelease props as Button. With pressOn="down", the active state's Event.Activated handler (or default's, if the active state has none) runs as soon as a mouse or touch press begins, and the native Activated for that press is swallowed. onRelease fires when that press ends or is cancelled, even if the pointer was dragged off the button.

<HoverButton
    pressOn="down"
    onRelease={() => setHeld(false)}
    default={{
        Size: UDim2.fromOffset(120, 40),
        BackgroundColor3: Color3.fromHex("#F4F5F7"),
        BackgroundTransparency: 0,
        AutoButtonColor: false,
        Event: {
            Activated: () => setHeld(true),
        },
    }}
/>

HoverButton has no disabled prop, so press-down is blocked instead while the merged state's Active is false. Gamepad and keyboard activation still go through the native Activated at the normal time and never get an onRelease.

Top-level events

HoverButton also takes an Event prop of its own, with the same shape as the ImageButton's Event table. It is merged with the Event of default and the active state set: when both define the same event, both handlers run, the state set's first, then the top-level one. HoverButton also forwards its ref to the rendered ImageButton.

Together, these let a HoverButton be the direct child of a Tooltip, with no Container wrapped around it:

<Tooltip content="Open the shop">
    <HoverButton
        default={{
            Size: UDim2.fromOffset(120, 40),
            BackgroundColor3: Color3.fromHex("#F4F5F7"),
            BackgroundTransparency: 0,
            AutoButtonColor: false,
        }}
        hover={{ BackgroundColor3: Color3.fromHex("#E1E4E8") }}
    />
</Tooltip>

Props

HoverButton-specific props

PropTypeDefaultDescription
defaultHoverButtonStatePropsrequiredBase ImageButton properties (plus border props), always applied.
hoverHoverButtonStatePropsundefinedProperties layered over default while the pointer is over the button and it is not selected.
focusHoverButtonStatePropsundefinedProperties layered over default while isSelected is true.
isSelectedbooleanfalseForces the focus state, whatever the hover state is.
namestring"HoverButton"Name of the rendered ImageButton instance.
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.
EventReact.InstanceEvent<ImageButton>undefinedEvent handlers merged with the state sets' Event. On a shared event, the state set's handler runs first, then this one.
childrenReact.ReactNodeundefinedContent rendered inside the button, after the border stroke.

HoverButtonStateProps is React.InstanceProps<ImageButton> & BorderElementProps.

Border props

These are accepted on HoverButton itself and in each of default, hover, and focus. The active state's value wins over the top-level value.

PropTypeDefaultDescription
borderThicknessnumberundefinedStroke thickness in pixels. A UIStroke is only rendered when this is greater than 0.
borderColorColor3undefinedStroke colour. When unset, the engine default (black) is used.
borderTransparencynumberundefinedStroke transparency. When unset, the engine default (0) is used.
borderPosition"inner" | "outer""inner""inner" draws the stroke inside the button's bounds; "outer" draws it outside them.

Shared props

The button also supports props inherited from the following interfaces.

InterfacePurpose
BorderElementPropsConfigures the optional UIStroke border, top-level or per state.

Behaviour

  • Only a fixed list of ImageButton properties is forwarded from the state sets: layout and sizing, background and native border, image properties, selection and gamepad navigation, Modal, AutoButtonColor, Active, Visible, ZIndex, Event, Change, and Tag. Other ImageButton properties are ignored even if they are set.
  • BorderSizePixel defaults to 0 when no state sets it.
  • Event handlers are merged as { ...default.Event, ...activeState.Event }. MouseEnter and MouseLeave are always wrapped internally to track hover. The wrapper still calls the active state's handler, and also the default state's handler when the two differ.
  • The top-level Event prop is merged on top of the state sets' events. For an event both define, the state set's handler runs first, then the top-level handler.
  • ref is forwarded to the rendered ImageButton.
  • Hover is tracked in local state through MouseEnter and MouseLeave.
  • The border props are never forwarded to the ImageButton. When drawn, the stroke is a UIStroke named Stroke, rendered as the button's first child.
  • Without pressOn="down", the event table is built exactly as described above. With it, Activated skips mouse and touch input, and an internal InputBegan handler runs the active state's own InputBegan first, then activates the button unless the merged Active is false.
  • There is no disabled state. To show a disabled look, put it in your own default, hover, and focus sets, for example by setting Active: false and matching colours.

Theme values

HoverButton is not themed and has no entry under theme.components. Its whole appearance comes from the default, hover, and focus prop sets and the border props.

GitHub Repository

On this page