macbetter

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

PropTypeDefaultDescription
aria-labelstring—Names the picker and the well's button.
valuestring—The color, as any CSS color. Pass it to control the well.
defaultValuestring"#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.
presetsstring[]Apple's system colorsPreset colors under the editor.
alphabooleantrueShows 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.
disabledbooleanfalseCan't be opened or changed.
childrenReactNode—Extra content at the end of the editor.

Data attributes

AttributeOnValues
data-sizeRootsm, md, lg
data-openWellPresent while the editor is open
data-presentationEditorpopover, sheet
data-starting-style, data-ending-styleEditorPresent while it opens or closes
data-channelSlidershue, alpha
data-selectedPresetsPresent on the preset that matches the color
data-out-of-gamutEvery partPresent while a drag holds a color outside the gamut

CSS variables

VariablemacOSiOSWhat it sets
--mb-color-well-width44px30pxWidth of the well
--mb-color-well-height--mb-control-height-sm30pxHeight of the well
--mb-color-well-panel-width240px300pxWidth of the editor
--color-picker-colorFrom ui-sdk: the current color, on every part