How a state is computed
Each state is an ordered list of operations on the RGBA pixels. First the canvas grows if the state has an outline, then brightness adds the value × 255 to R, G and B, contrast scales around mid grey (128), saturation moves each channel away from or towards the pixel's luma (0.2126 R + 0.7152 G + 0.0722 B), an optional colour overlay is mixed in, the pixels are offset, the outline is drawn as a square dilation of the shape, and finally alpha is multiplied. Fully transparent pixels are never recoloured.
Engines expect these images in fixed slots. Godot 4's TextureButton has texture_normal, texture_hover, texture_pressed, texture_disabled and texture_focused; a disabled button without its own texture shows the normal one, and the focused texture is drawn on top of the base texture. Unity's UI Button with Transition = Sprite Swap takes a normal sprite on its Target Graphic plus Highlighted, Pressed and Disabled sprites.
Two WCAG 2.2 rules are worth knowing even though games are not web pages. Success Criterion 1.4.3 exempts text that is part of an inactive (disabled) component from its contrast minimum, which is why a faded disabled state is acceptable. 2.4.13 Focus Appearance (level AAA) asks for a focus indicator at least as large as a 2 CSS px perimeter of the component with a 3:1 contrast between focused and unfocused pixels; the default ring is 2 px, and #3182f6 against white measures 3.71:1.
- Brightness
- Added to every channel: +0.1 means +25.5 levels.
- Saturation
- +0.08 pushes each channel 8 % further from the luma; −1 turns the pixel grey.
- Offset
- Moves the pixels inside the same canvas; what leaves is dropped, what enters is transparent.
- Outline
- A ring of the chosen colour and width around the opaque shape; the canvas grows by that width.
Example: one pixel through the five default states
A 40 × 16 button filled with the colour (200, 120, 40). The numbers are what the exported PNGs contain:
state pixel (R, G, B, A) size operations
normal (200, 120, 40, 255) 40 × 16 none
hover (231, 145, 58, 255) 40 × 16 brightness +0.1 (+25.5), saturation +0.08
pressed (174, 94, 14, 255) 40 × 16 brightness −0.1, offset 0,1 → row 0 becomes transparent
disabled (131, 131, 131, 128) 40 × 16 saturation −1, alpha × 0.5
focus (200, 120, 40, 255) 44 × 20 canvas +2 px per side, 2 px ring (49, 130, 246)
disabled grey 0.2126 × 200 + 0.7152 × 120 + 0.0722 × 40 = 131.2 → 131; alpha 255 × 0.5 = 127.5 → 128
hover green luma 156.7 + (145.5 − 156.7) × 1.08 = 144.6 → 145
The focus image is 4 px wider and taller than the others, and its ring covers the corner pixels too, because the outline is a square dilation. states.json records each state's size and padding so the difference is never a surprise.
Use the states in Godot or Unity
- Godot 4: add a
TextureButton and assign normal.png, hover.png, pressed.png and disabled.png to texture_normal, texture_hover, texture_pressed and texture_disabled. - Godot 4 draws
texture_focused over the base texture and its documentation recommends a partly transparent image such as an outline. focus.png contains the whole button, so for that slot erase the button inside the ring in an image editor and keep the 44 × 20 canvas. - Unity UI: on the Button set Transition to Sprite Swap, put
normal.png on the Target Graphic's Image and hover.png, pressed.png, disabled.png into Highlighted Sprite, Pressed Sprite and Disabled Sprite. - If the button must resize, give each state PNG the same nine-slice borders in the 9-slice editor before assigning them, because the states carry no borders of their own.
Check the result
- Every
rect in states.json cuts exactly its state out of button-states.png, and states.hover.ops lists the values used. normal.png is the source byte for byte (in Firefox semi-transparent pixels can differ by one alpha-rounding step).- Run the game and hover, press and disable the button: nothing jumps, and the pressed state moves by exactly the offset.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|
| The pressed image loses its bottom row | The 1 px offset moves the pixels inside the same canvas, so the last row falls off and row 0 becomes transparent | Compare the bottom edge of pressed.png with the source | Leave one transparent row under the art, or set the offset to 0 and move the label in the engine instead |
|---|
| The focused button jumps or looks doubled | focus.png is larger (44 × 20 for a 40 × 16 button) and Godot draws the focus texture over the base | states.json → states.focus.size | Centre it by −outline px, or use a ring-only overlay as described above |
|---|
| The disabled button almost vanishes on a dark background | Alpha 0.5 lets the background show through | Look at it over your real background, not on white | Raise the disabled alpha (0.7) or keep alpha 1 and only desaturate |
|---|
| Hover is barely visible on a light button | Brightness is additive and channels already near 255 clip | Pixel values above about 230 in the source | Use an overlay colour or contrast for hover instead of brightness |
|---|
Other ways to do it
| Option | When it is the better choice |
|---|
| Unity Transition = Color Tint | The states only differ in colour: no extra images, but a tint multiplies colours and cannot move pixels (pressed offset) or add a ring. |
|---|
| Draw each state by hand in an art program | The states differ in shape: a bevel that flips, an icon that changes, a glow. Pixel arithmetic cannot draw what is not in the source. |
|---|
What this page does not cover
- No per-state nine-slice borders: set them on the state PNGs in the 9-slice editor.
- Engine use was not tested; the slot names above come from the Godot and Unity documentation.
Versions and sources
Measured in Chromium for docs/UI-LAB.md on a 40 × 16 button: normal is byte-identical to the source, hover is brighter, pressed is offset with the vacated rows cleared, disabled is grey at alpha 128, focus is 44 × 20 with a #3182f6 ring and untouched artwork, and all five rects in states.json cut exactly their state out of the strip. The example values above were recomputed with the same module. Godot and Unity import were not run.
Official documentation used
Button State Generator
Turn one button image into normal, hover, pressed, disabled and focus variants, as single PNGs and one packed strip.
How it works
- Add images
- Adjust settings and review the result
- Download a new file
Supported formats
PNG / JPG / WebP / AVIF / HEIC → PNG + JSON (ZIP) — Input decoding depends on browser support and optional engines.
What this tool does
Turn one button image into normal, hover, pressed, disabled and focus variants, as single PNGs and one packed strip.
Implementation and verified quality
advanced · brightness, contrast, saturation, overlay, offset and square outline ops / MaxRects strip. Quality qualification is in progress. Pro/Flagship quality has not been established.
Before you export
Fixed pixel operations: brightness, contrast, saturation, colour overlay, offset and outline. It does not draw artwork that is not there.