Buttons and controls
Color Well
Show a color and open an editor to change it.
import { ColorWell } from "macbetter/color-well"
export default function Example() {
return <ColorWell aria-label="Highlight color" defaultValue="#ee5700" />
}When to use it
Use a color well wherever someone picks a color for something, like a highlight, a label or a shape's fill. The well always shows the current color; pressing it opens an editor in a popover, and every change applies as it happens.
- Put the well next to a label that names what it colors.
- Offer presets that fit the context, like your brand's colors, so most people never need the full editor.
- Turn off the opacity slider with
alpha={false}when transparency makes no sense, like for a text color.
Usage
import { ColorWell } from "macbetter/color-well"aria-label names both the color picker and the well's button. The value is a CSS color string; format="hex" (the default) writes it as hex, with an alpha pair when it's translucent.
In a list of properties
Fill
Stroke
Shadow
import { ColorWell } from "macbetter/color-well"
const row = { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 24 }
export default function Example() {
return (
<div style={{ display: "flex", flexDirection: "column", gap: 12, width: "100%", maxWidth: 280 }}>
<div style={row}>
<span>Fill</span>
<ColorWell aria-label="Fill" defaultValue="#028f71" />
</div>
<div style={row}>
<span>Stroke</span>
<ColorWell aria-label="Stroke" defaultValue="#0a1c13" />
</div>
<div style={row}>
<span>Shadow</span>
<ColorWell aria-label="Shadow" defaultValue="#00000066" />
</div>
</div>
)
}Presets
presets replaces Apple's system colors with your own. Pass [] for none.
import { ColorWell } from "macbetter/color-well"
const brand = ["#ee5700", "#f67f01", "#028f71", "#52c6a0", "#0a1c13", "#f1f7f3"]
export default function Example() {
return <ColorWell aria-label="Brand color" alpha={false} presets={brand} defaultValue="#028f71" />
}Controlled
Pass value and onValueChange, which runs on every step of a drag. Use onValueCommit to save once a drag ends.
#5856d6
"use client"
import { ColorWell } from "macbetter/color-well"
import { useState } from "react"
/** Black or white, whichever reads better on the color. */
function inkFor(hex: string) {
const [r, g, b] = [1, 3, 5].map((i) => {
const c = parseInt(hex.slice(i, i + 2), 16) / 255
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
})
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
return luminance > 0.18 ? "#000000" : "#ffffff"
}
export default function Example() {
const [color, setColor] = useState("#5856d6")
return (
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
<ColorWell aria-label="Folder color" value={color} onValueChange={setColor} />
<span
style={{
padding: "6px 12px",
borderRadius: 999,
background: color,
color: inkFor(color),
fontFamily: "var(--mb-font-mono)",
fontSize: 13,
}}
>
{color}
</span>
</div>
)
}Platform behavior
- macOS. A bezeled well, like NSColorWell, with the color inset in a rounded rectangle. It stays pressed while its popover is open. The editor floats on a translucent material: a saturation and brightness area, hue and opacity sliders, a hex field, a screen color picker where the browser supports one, and presets.
- iPadOS and iOS. UIColorWell's circle of color inside a spectrum ring. The editor has larger thumbs and touch targets. On an iPhone it opens as a bottom sheet; on iPad it's a popover.
- Every platform. Transparent colors show over a checkerboard, and thumbs stay inside their tracks at both ends.
Accessibility
- The well is a button that opens the editor, and focus moves into it. Escape closes it and returns focus to the well.
- The area's thumb is a two-dimensional slider named for both channels, like "Saturation 76%, Brightness 96%". Each slider is a slider, with arrow keys, Shift for ten steps and Page Up and Page Down.
- Presets are a listbox, named by color, like "vivid red". The arrow keys move between them.
- The hex field accepts any CSS color when you press Enter or leave it.
API
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | — | Names the picker and the well's button. |
value | string | — | The color, as any CSS color. Pass it to control the well. |
defaultValue | string | "#000000" | The color at first. |
onValueChange | (value, details) => void | — | Runs on every step of a drag or a key. |
onValueCommit | (value, details) => void | — | Runs once an interaction ends. For saving. |
presets | string[] | Apple's system colors | Preset colors under the editor. |
alpha | boolean | true | Shows the opacity slider. |
size | "sm" | "md" | "lg" | "md" | Size of the well. |
format | "hex" | "rgb" | "hsl" | "oklch" | … | "hex" | How the value is written. |
space | "hsv" | "hsl" | "srgb" | "oklch" | "hsv" | The space the editor works in. |
presentation | "popover" | "sheet" | "auto" | "auto" | auto opens a sheet on small touch screens. |
side | "top" | "bottom" | "left" | "right" | "bottom" | Where the popover opens. |
align | "start" | "center" | "end" | "start" | How the popover lines up with the well. |
disabled | boolean | false | Can't be opened or changed. |
children | ReactNode | — | Extra content at the end of the editor. |
Data attributes
| Attribute | On | Values |
|---|---|---|
data-size | Root | sm, md, lg |
data-open | Well | Present while the editor is open |
data-presentation | Editor | popover, sheet |
data-starting-style, data-ending-style | Editor | Present while it opens or closes |
data-channel | Sliders | hue, alpha |
data-selected | Presets | Present on the preset that matches the color |
data-out-of-gamut | Every part | Present while a drag holds a color outside the gamut |
CSS variables
| Variable | macOS | iOS | What it sets |
|---|---|---|---|
--mb-color-well-width | 44px | 30px | Width of the well |
--mb-color-well-height | --mb-control-height-sm | 30px | Height of the well |
--mb-color-well-panel-width | 240px | 300px | Width of the editor |
--color-picker-color | From ui-sdk: the current color, on every part |