Docs / Components / 06

ComposeColorPicker

A full color picker: a hue/saturation/brightness area in one of three styles, then an alpha slider, a before/after preview, a hex field and preset swatches. It works in HSV, so dragging to gray or black and back keeps the hue.

  • State or value
  • Wheel · Ring · Square
  • HSV, keeps hue
  • Composable parts

Preview

How it works

  • Works in HSV. The picker stores hue, saturation, value and alpha (HsvColor) rather than a Color, so dragging down to gray or black and back up keeps the hue you had.
  • Two ways to hold the color. color + onColorChange for a plain value, or rememberComposeColorPickerState() when you want to read and set state.color / state.hsvColor at any time. onColorChangeFinished runs when a drag ends, a swatch is tapped or a hex code is entered.
  • Three styles for the main area. Wheel is a hue/saturation disc with a brightness slider, Ring is a hue ring around a saturation/brightness square, and Square is a panel with a hue slider.
  • Toggle the extras. Alpha slider, before/after preview (originalColor, tap the old half to go back), hex field and swatches can each be shown or hidden.
  • Or build your own. Every part (SaturationValuePanel, HueSlider, AlphaSlider, ColorHexField, ColorSwatches…) is public and stateless, taking hsvColor + onHsvColorChange, so you can lay them out however you like.

Setup

Add the library once (see Getting started), then import from com.nameisjayant.components.colorpicker:

Imports
import com.nameisjayant.components.colorpicker.*

Quick start

ComposeColorPicker.kt
// Plain value + callback
var color by remember { mutableStateOf(Color(0xFF3D5AFE)) }
ComposeColorPicker(color = color, onColorChange = { color = it })

// Or a state you can read and set any time
val state = rememberComposeColorPickerState(initialColor = Color(0xFF3D5AFE))
ComposeColorPicker(
    state = state,
    style = ColorPickerStyle.Ring,                    // Wheel | Ring | Square
    originalColor = Color(0xFF3D5AFE),                // split preview; tap the old half to go back
    onColorChangeFinished = { recents.add(it) },      // finger lifted, swatch tapped or hex entered
)
state.color = Color.Red                               // or state.hsvColor = HsvColor(…)

Choose what's shown

ComposeColorPicker.kt
ComposeColorPicker(
    state = state,
    showAlpha = false,                                  // no alpha slider; the hex field shows RRGGBB
    showPreview = true,
    showHexField = true,
    swatches = brandColors,                             // emptyList() for none
    colors = ComposeColorPickerDefaults.accentColors(MaterialTheme.colorScheme.tertiary),
    thumbSize = 24.dp, sliderHeight = 20.dp, ringWidth = 24.dp,
    panelShape = RoundedCornerShape(8.dp), swatchSize = 32.dp, spacing = 12.dp,
)

The hex field accepts RGB, ARGB, RRGGBB and AARRGGBB, with or without #. Codes without alpha keep the current alpha.

In a dialog

ComposeColorPicker.kt
if (show) {
    val state = rememberComposeColorPickerState(initialColor = current)
    ComposeColorPickerDialog(
        onDismissRequest = { show = false },
        title = { Text("Pick a color") },
        confirmButton = { TextButton(onClick = { save(state.color); show = false }) { Text("OK") } },
        dismissButton = { TextButton(onClick = { show = false }) { Text("Cancel") } },
    ) {
        ComposeColorPicker(state, Modifier.padding(horizontal = 24.dp), originalColor = current)
    }
}

The dialog's content scrolls when the screen is too short for it.

Build your own picker from the parts

Every piece is public and stateless (hsvColor + onHsvColorChange), so you can lay them out however you like:

ComposeColorPicker.kt
var hsv by remember { mutableStateOf(HsvColor.from(Color(0xFF3D5AFE))) }

Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
    SaturationValuePanel(hsv, { hsv = it }, aspectRatio = 2f)
    HueSlider(hsv, { hsv = it })
    SaturationSlider(hsv, { hsv = it })
    BrightnessSlider(hsv, { hsv = it })
    AlphaSlider(hsv, { hsv = it })
    Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
        ColorPreview(hsv.toColor())
        ColorHexField(hsv, { hsv = it }, Modifier.weight(1f))
    }
    ColorSwatches(ComposeColorPickerDefaults.Swatches, selectedColor = hsv.toColor(), onSwatchClick = { hsv = HsvColor.from(it, hsv) })
}

Also available: HueSaturationWheel and HueRingPanel. HsvColor(hue, saturation, value, alpha) has toColor(), toArgb(), copy(...) and HsvColor.from(color, previous), which keeps the previous hue for grays.

All parameters

ParameterDefaultDescription
state or color + onColorChangePick the overload you need
onColorChangenull(state overload) Every change while dragging
onColorChangeFinishednullWhen a drag ends, a swatch is tapped or a hex code is entered
styleWheelWheel (disc + brightness slider), Ring (hue ring around a square), Square (panel + hue slider)
enabledtrueDimmed and ignores touch when false
showAlpha / showPreview / showHexFieldtrueToggle each section
originalColornullShown beside the current color in the preview; tap it to go back
swatchesComposeColorPickerDefaults.Swatches16 everyday colors; emptyList() hides the row
colorsComposeColorPickerDefaults.colors()thumbColor, thumbBorderColor, borderColor, checkerboardLightColor, checkerboardDarkColor, selectedSwatchBorderColor, fieldContainerColor, fieldTextColor, fieldLabelColor, fieldBorderColor, fieldFocusedBorderColor
thumbSize / sliderHeight / ringWidth28 / 24 / 28.dpThumb and track sizes
panelShapeRoundedCornerShape(16.dp)Corners of the saturation/brightness panel
swatchSize / spacing36.dp / 16.dpSwatch size and gap between sections

The picker fills the width its modifier gives it. Sliders are adjustable for screen readers ("Hue, 210°", "Opacity, 80%"), panels read their hex code, and swatches are radio buttons named by their hex code. State survives configuration changes and process death, hue included.

API

The public signatures, straight from the library sources:

com.nameisjayant.components.colorpicker
@Composable
public fun ComposeColorPicker(
    state: ComposeColorPickerState,
    modifier: Modifier = Modifier,
    onColorChange: ((Color) -> Unit)? = null,
    onColorChangeFinished: ((Color) -> Unit)? = null,
    style: ColorPickerStyle = ColorPickerStyle.Wheel,
    enabled: Boolean = true,
    showAlpha: Boolean = true,
    showPreview: Boolean = true,
    showHexField: Boolean = true,
    originalColor: Color? = null,
    swatches: List<Color> = ComposeColorPickerDefaults.Swatches,
    colors: ComposeColorPickerColors = ComposeColorPickerDefaults.colors(),
    thumbSize: Dp = ComposeColorPickerDefaults.ThumbSize,
    sliderHeight: Dp = ComposeColorPickerDefaults.SliderHeight,
    ringWidth: Dp = ComposeColorPickerDefaults.RingWidth,
    panelShape: Shape = ComposeColorPickerDefaults.PanelShape,
    swatchSize: Dp = ComposeColorPickerDefaults.SwatchSize,
    spacing: Dp = ComposeColorPickerDefaults.Spacing,
)

@Composable
public fun ComposeColorPicker(
    color: Color,
    onColorChange: (Color) -> Unit,
    modifier: Modifier = Modifier,
    onColorChangeFinished: ((Color) -> Unit)? = null,
    style: ColorPickerStyle = ColorPickerStyle.Wheel,
    enabled: Boolean = true,
    showAlpha: Boolean = true,
    showPreview: Boolean = true,
    showHexField: Boolean = true,
    originalColor: Color? = null,
    swatches: List<Color> = ComposeColorPickerDefaults.Swatches,
    colors: ComposeColorPickerColors = ComposeColorPickerDefaults.colors(),
    thumbSize: Dp = ComposeColorPickerDefaults.ThumbSize,
    sliderHeight: Dp = ComposeColorPickerDefaults.SliderHeight,
    ringWidth: Dp = ComposeColorPickerDefaults.RingWidth,
    panelShape: Shape = ComposeColorPickerDefaults.PanelShape,
    swatchSize: Dp = ComposeColorPickerDefaults.SwatchSize,
    spacing: Dp = ComposeColorPickerDefaults.Spacing,
)

@Composable
public fun rememberComposeColorPickerState(initialColor: Color

@Composable
public fun ComposeColorPickerDialog(
    onDismissRequest: () -> Unit,
    confirmButton: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    dismissButton: (@Composable () -> Unit)? = null,
    title: (@Composable () -> Unit)? = null,
    shape: Shape = RoundedCornerShape(28.dp),
    containerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
    tonalElevation: Dp = 6.dp,
    properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
    content: @Composable ColumnScope.() -> Unit,
)