Multiplatform color picker library for Android, iOS, and Desktop (JVM), built with Compose Multiplatform and Material 3.
- Compose Multiplatform (Android, iOS, Desktop/JVM)
- Material 3 theming via
ColorPickerDefaults - HSL, RGB, CMYK, and LAB color models
- Alpha channel support
- Zero-drift editing:
ColorPickerStatekeeps the authoritative color in the space you edited, so edit-in-X-read-X is always exact (conversions themselves are float-based) - Unidirectional data flow with
ColorPickerState - Hex string parsing and formatting
- Color picker dialog
- Accessibility semantics and RTL layout support
// build.gradle.kts
implementation("codes.side:colorpicker:1.0.0")In a Kotlin Multiplatform project, add it to commonMain:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("codes.side:colorpicker:1.0.0")
}
}
}Published targets: android, jvm, iosArm64, iosSimulatorArm64.
@Composable
fun MyScreen() {
val state = rememberColorPickerState(
initialColor = HslColor(hue = 200f, saturation = 0.8f, lightness = 0.5f)
)
Column {
HslColorPicker(state = state)
ColorSwatch(
color = state.hslColor.toComposeColor(),
modifier = Modifier.size(48.dp)
)
}
}All color models use Float for full precision. Integer accessors and factories are provided for convenience.
val color = HslColor(hue = 210f, saturation = 0.8f, lightness = 0.5f, alpha = 1f)
// hue: [0, 360], saturation/lightness/alpha: [0, 1]
// Integer accessors
color.intHue // 210
color.intSaturation // 80
color.intLightness // 50
color.intAlpha // 255
// From integers
HslColor.fromInt(hue = 210, saturation = 80, lightness = 50, alpha = 255)val color = RgbColor(red = 0.2f, green = 0.5f, blue = 0.8f, alpha = 1f)
// All components: [0, 1]
color.intRed // 51
color.intGreen // 128
color.intBlue // 204
RgbColor.fromInt(red = 51, green = 128, blue = 204)val color = CmykColor(cyan = 0.3f, magenta = 0.6f, yellow = 0.1f, key = 0.2f)
// All components: [0, 1]
CmykColor.fromInt(cyan = 30, magenta = 60, yellow = 10, key = 20)val color = LabColor(l = 53.23f, a = 80.11f, b = 67.22f)
// l: [0, 100], a: [-128, 127], b: [-128, 127], alpha: [0, 1]
LabColor.fromInt(l = 53, a = 80, b = 67)Conversions are extension functions. They operate on floats end to end — nothing is quantized to integers until you explicitly ask for an ARGB Int or a hex string. Like any color space conversion, a cross-space round trip is not guaranteed to be bit-exact; the zero-drift guarantee comes from ColorPickerState's origin tracking (see Architecture).
val hsl = HslColor(hue = 0f, saturation = 1f, lightness = 0.5f)
val rgb = hsl.toRgb()
val cmyk = rgb.toCmyk()
val lab = rgb.toLab()
val argb = rgb.toArgbInt()
// Compose interop, both ways
val composeColor: Color = hsl.toComposeColor()
val backToHsl: HslColor = composeColor.toHslColor()
val backToRgb: RgbColor = composeColor.toRgbColor()
val backToCmyk: CmykColor = composeColor.toCmykColor()
val backToLab: LabColor = composeColor.toLabColor()val rgb = RgbColor(red = 0.2f, green = 0.5f, blue = 0.8f)
// Formatting: any PickerColor or packed ARGB Int
rgb.toHexString() // "#FF3380CC" (#AARRGGBB, alpha first)
rgb.toHexString(includeAlpha = false) // "#3380CC"
0xFF3380CC.toInt().toHexColorString() // "#FF3380CC"
// Parsing: accepts #RGB, #RRGGBB, and #AARRGGBB; the '#' is optional
"#3380CC".toRgbColorOrNull() // RgbColor, alpha defaults to FF
"#ABC".toRgbColorOrNull() // shorthand, expands to #AABBCC
"not a color".toRgbColorOrNull() // null, never throws
"#3380CC".toRgbColor() // throws IllegalArgumentException on invalid inputEach color model has a ready-made picker that stacks its channel sliders (plus an optional alpha slider):
val state = rememberColorPickerState()
HslColorPicker(
state = state,
showAlpha = true,
coloringMode = ColoringMode.Independent, // or Contextual
)
RgbColorPicker(state = state, showAlpha = true)
CmykColorPicker(state = state, showAlpha = true)
LabColorPicker(state = state, showAlpha = true)ColoringMode controls the slider gradients: Independent shows each channel's full range regardless of the other channels, Contextual previews the actual resulting color at each position.
Every channel is available as a standalone slider. Compose any subset against a shared state:
// HSL
HueSlider(state = state)
SaturationSlider(state = state)
LightnessSlider(state = state)
// RGB
RedSlider(state = state)
GreenSlider(state = state)
BlueSlider(state = state)
// CMYK
CyanSlider(state = state)
MagentaSlider(state = state)
YellowSlider(state = state)
KeySlider(state = state)
// LAB
LightnessLabSlider(state = state)
LabASlider(state = state)
LabBSlider(state = state)
// Alpha (works with any origin space)
AlphaSlider(state = state)Sliders expose slots and semantics for customization:
HueSlider(
state = state,
label = { SliderLabel("Hue") }, // leading label slot (null to hide)
valueLabel = { SliderValueLabel("200°") }, // trailing value slot (null to hide)
semanticLabel = "Hue", // accessibility label
semanticValueText = "200°", // accessibility value announcement
)Renders a color over a transparency checkerboard:
ColorSwatch(
color = state.hslColor.toComposeColor(),
modifier = Modifier.fillMaxWidth().height(48.dp),
contentDescription = "Selected color",
)A Material 3 AlertDialog with an HSL picker and a live swatch. Callbacks come first; everything else has defaults:
ColorPickerDialog(
onColorSelected = { hsl -> /* confirmed color */ },
onDismiss = { /* close */ },
initialColor = HslColor(hue = 200f, saturation = 0.8f, lightness = 0.5f),
title = "Pick a Color",
confirmText = "Select",
dismissText = "Cancel",
showAlpha = true,
)In-progress edits inside the dialog survive configuration changes; passing a new initialColor resets the picker.
All pickers and sliders accept colors and shapes built with ColorPickerDefaults, which derive from MaterialTheme by default:
HslColorPicker(
state = state,
colors = ColorPickerDefaults.colors(
checkerboardLight = Color.White,
checkerboardDark = Color.LightGray,
),
shapes = ColorPickerDefaults.shapes(
trackShape = RoundedCornerShape(4.dp),
swatchShape = RoundedCornerShape(8.dp),
),
)ColorPickerState is the single source of truth. It reads and writes each color space natively, with no round-trip conversions.
val state = rememberColorPickerState()
// Read any color space (derived from the authoritative color)
state.hslColor
state.rgbColor
state.cmykColor
state.labColor
state.argbInt
state.pickerColor // the authoritative color, in whichever space was last written
// Per-channel updates (NaN ignored, values clamped)
state.updateHue(180f)
state.updateSaturation(0.5f)
state.updateLightness(0.5f)
state.updateRed(1f)
state.updateGreen(0f)
state.updateBlue(0f)
state.updateCyan(0.3f)
state.updateMagenta(0.6f)
state.updateYellow(0.1f)
state.updateKey(0.2f)
state.updateLabLightness(50f)
state.updateLabA(20f)
state.updateLabB(-30f)
state.updateAlpha(0.5f) // keeps the current origin space
// Whole-color updates (the written space becomes the origin)
state.updateFromHsl(HslColor(hue = 0f, saturation = 1f, lightness = 0.5f))
state.updateFromRgb(RgbColor(1f, 0f, 0f))
state.updateFromCmyk(cmykColor)
state.updateFromLab(labColor)
state.updateFromArgbInt(0xFFFF0000.toInt())
// True while the user is dragging a slider
state.isInteractingColorPickerState has a public constructor, so it can also be created and held outside of composition (e.g. in a ViewModel).
Use rememberSaveableColorPickerState() to keep the state across configuration changes and process death on platforms that provide saved-instance-state support (primarily Android). On other platforms it behaves like rememberColorPickerState within the composition. The saver preserves the authoritative color space, not just the visible color.
Color space conversions are inherently lossy when values are quantized to integers, and even with floats, transcendental functions (used in LAB) introduce IEEE 754 rounding errors. Industry-standard tools (Photoshop, CSS Color Level 4, Sass) solve this the same way we do:
Store colors in their authored color space. Convert forward only. Never convert back.
ColorPickerState tracks which color space was last written to (the origin). When you read a different space, it converts forward once from the origin. The origin value is never re-derived from a conversion.
User drags Red slider
-> the authoritative color is written as RGB (origin = RGB, zero conversions)
-> UI reads hslColor -> converts RGB->HSL once (forward only)
-> UI reads rgbColor -> returns the authoritative RGB value as-is (zero conversions)
This means:
- Editing in RGB and reading back RGB produces the exact original value
- Editing in HSL and reading back HSL produces the exact original value
- Cross-space reads involve a single forward conversion, never a round-trip
- No precision loss accumulates over time, regardless of how many edits are made
For more details, see:
- CSS Color Module Level 4 -- the W3C spec mandates the same approach
- Sass Color Spaces -- stores colors in their original space
The View-based codes.side:andcolorpicker artifact (XML HSLColorPickerSeekBar and friends) is discontinued. This library is a full Compose Multiplatform rewrite published under new coordinates:
- implementation("codes.side:andcolorpicker:0.6.2")
+ implementation("codes.side:colorpicker:1.0.0")There is no 1:1 API mapping — migrate by concept:
| andcolorpicker (View-based) | colorpicker (Compose) |
|---|---|
HSLColorPickerSeekBar (hslMode = hue/saturation/lightness) |
HueSlider / SaturationSlider / LightnessSlider, or HslColorPicker for all three |
RGBColorPickerSeekBar |
RedSlider / GreenSlider / BlueSlider, or RgbColorPicker |
CMYKColorPickerSeekBar |
CyanSlider / MagentaSlider / YellowSlider / KeySlider, or CmykColorPicker |
LABColorPickerSeekBar |
LightnessLabSlider / LabASlider / LabBSlider, or LabColorPicker |
HSLAlphaColorPickerSeekBar |
AlphaSlider |
PickerGroup + registerPickers |
Pass one ColorPickerState to every component — they stay in sync automatically |
SwatchView |
ColorSwatch |
OnColorPickListener / addListener |
Read state.hslColor (or any other space) — it is Compose snapshot state, so composition recomposes automatically; use snapshotFlow outside composition |
IntegerHSLColor and friends |
HslColor, RgbColor, CmykColor, LabColor (float-based, with fromInt factories) |
hslColoringMode = pure / output |
ColoringMode.Independent / ColoringMode.Contextual |
Copyright 2020 Illia Achour
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.