All components
Color Picker
Swatch trigger and checkerboard swatch grid with a selection indicator, from 2xs to 2xl.
1234567891011121314151617181920212223242526272829303132333435363738import { ColorPicker, parseColor } from "#components/ui/ark-ui/color-picker.tsx";
const swatches = ["#ef4444", "#22c55e", "#6366f1", "#a855f7"];
export function Example() { return ( // variant/size resolve once on Root; every part inherits them via // context — SwatchIndicator ships a default check and only shows on the // selected swatch <ColorPicker.Root defaultValue={parseColor("#6366f1")} variant="outline" size="md"> <ColorPicker.Label>Brand color</ColorPicker.Label> <ColorPicker.Control> <ColorPicker.Trigger> <ColorPicker.ValueSwatch /> <ColorPicker.ValueText /> </ColorPicker.Trigger> </ColorPicker.Control> <ColorPicker.SwatchGroup> {swatches.map((hex) => ( <ColorPicker.SwatchTrigger key={hex} value={hex}> <ColorPicker.Swatch value={hex}> <ColorPicker.SwatchIndicator /> </ColorPicker.Swatch> </ColorPicker.SwatchTrigger> ))} </ColorPicker.SwatchGroup> <ColorPicker.HiddenInput /> </ColorPicker.Root> );}
// the full picker popup composes the same way:// <ColorPicker.Positioner>// <ColorPicker.Content>// <ColorPicker.Area><ColorPicker.AreaBackground /><ColorPicker.AreaThumb /></ColorPicker.Area>// <ColorPicker.ChannelSlider channel="hue">…</ColorPicker.ChannelSlider>// </ColorPicker.Content>// </ColorPicker.Positioner>Variants
outline
subtle
Sizes
2xs
xs
sm
md
lg
xl
2xl
Install
1npx stylexblocks add color-pickerSource
components/ui/ark-ui/color-picker.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678import * as stylex from "@stylexjs/stylex";import { ColorPicker as ArkColorPicker } from "@ark-ui/react/color-picker";import { Portal } from "@ark-ui/react/portal";import { createSlotRecipeContext } from "#lib/ark-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles, type SlotRecipeVariants, type StyleOverrideProps,} from "#lib/recipe-props.ts";import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { colors } from '@stylexblocks/ui/tokens/colors.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { semanticShadows } from '@stylexblocks/ui/semantic-tokens/shadows.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'import { zIndices } from '@stylexblocks/ui/tokens/z-indices.stylex.ts'
const colorPickerRoot = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._1_5, },})
const colorPickerLabel = stylex.create({ base: { color: semanticColors.fg, fontWeight: fontWeights.medium, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, },})
const colorPickerValueText = stylex.create({ base: { textAlign: 'start', },})
const colorPickerControl = stylex.create({ base: { display: 'flex', alignItems: 'center', flexDirection: 'row', gap: spacing._2, position: 'relative', },})
const colorPickerSwatchTrigger = stylex.create({ base: { display: 'flex', alignItems: 'center', justifyContent: 'center', },})
const colorPickerTrigger = stylex.create({ base: { display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'row', flexShrink: 0, gap: spacing._2, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, minHeight: 'var(--input-height)', minWidth: 'var(--input-height)', paddingInline: spacing._1, borderRadius: semanticRadii.controlMd, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, '--focus-color': colorPalette.focusRing, },})
const colorPickerTriggerSizes = stylex.create({ '2xs': { '--input-height': sizes._5, }, xs: { '--input-height': sizes._6, }, sm: { '--input-height': sizes._7, }, md: { '--input-height': sizes._8, }, lg: { '--input-height': sizes._10, }, xl: { '--input-height': sizes._12, }, '2xl': { '--input-height': sizes._16, },})
const colorPickerTriggerVariants = stylex.create({ outline: { borderWidth: '1px', }, subtle: { borderWidth: '1px', borderColor: 'transparent', backgroundColor: semanticColors.bgMuted, },})
const colorPickerContent = stylex.create({ base: { display: 'flex', flexDirection: 'column', width: sizes._64, padding: spacing._4, gap: spacing._3, '--color-picker-z-index': zIndices.layer3, zIndex: 'calc(var(--color-picker-z-index) + var(--layer-index, 0))', },})
const colorPickerArea = stylex.create({ base: { height: '180px', borderRadius: semanticRadii.controlMd, overflow: 'hidden', },})
const colorPickerAreaSizes = stylex.create({ '2xs': { '--thumb-size': sizes._3, }, xs: { '--thumb-size': sizes._3_5, }, sm: { '--thumb-size': sizes._3_5, }, md: { '--thumb-size': sizes._3_5, }, lg: { '--thumb-size': sizes._3_5, }, xl: { '--thumb-size': sizes._3_5, }, '2xl': { '--thumb-size': sizes._3_5, },})
const colorPickerAreaThumb = stylex.create({ base: { borderRadius: radii.full, height: 'var(--thumb-size)', width: 'var(--thumb-size)', borderWidth: '2px', borderColor: colors.white, boxShadow: semanticShadows.sm, '--focus-ring-color': colors.white, },})
const colorPickerAreaBackground = stylex.create({ base: { height: sizes.full, },})
const colorPickerChannelSlider = stylex.create({ base: { borderRadius: semanticRadii.controlMd, flex: 1, },})
const colorPickerChannelSliderSizes = stylex.create({ '2xs': { '--slider-height': sizes._3, '--thumb-size': sizes._3, }, xs: { '--slider-height': sizes._3_5, '--thumb-size': sizes._3_5, }, sm: { '--slider-height': sizes._3_5, '--thumb-size': sizes._3_5, }, md: { '--slider-height': sizes._3_5, '--thumb-size': sizes._3_5, }, lg: { '--slider-height': sizes._3_5, '--thumb-size': sizes._3_5, }, xl: { '--slider-height': sizes._3_5, '--thumb-size': sizes._3_5, }, '2xl': { '--slider-height': sizes._3_5, '--thumb-size': sizes._3_5, },})
const colorPickerChannelSliderTrack = stylex.create({ base: { height: 'var(--slider-height)', borderRadius: 'inherit', boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.1)', },})
const colorPickerChannelText = stylex.create({ base: { fontSize: fontSizes.xs, lineHeight: lineHeights.xs, color: semanticColors.fgMuted, fontWeight: fontWeights.medium, textTransform: 'capitalize', },})
const colorPickerSwatchGroup = stylex.create({ base: { display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: spacing._2, },})
const colorPickerSwatch = stylex.create({ base: { width: 'var(--swatch-size)', height: 'var(--swatch-size)', boxShadow: 'inset 0 0 0 1px rgba(0, 0, 0, 0.1)', '--checker-size': '8px', '--checker-bg': semanticColors.bg, '--checker-fg': semanticColors.bgEmphasized, backgroundImage: 'linear-gradient(var(--color), var(--color)), repeating-conic-gradient(var(--checker-fg) 0%, var(--checker-fg) 25%, var(--checker-bg) 0%, var(--checker-bg) 50%) 0% 50% / var(--checker-size) var(--checker-size) !important', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, borderRadius: semanticRadii.controlSm, },})
const colorPickerSwatchSizes = stylex.create({ '2xs': { '--swatch-size': sizes._4_5, }, xs: { '--swatch-size': sizes._5, }, sm: { '--swatch-size': sizes._6, }, md: { '--swatch-size': sizes._7, }, lg: { '--swatch-size': sizes._7, }, xl: { '--swatch-size': sizes._8, }, '2xl': { '--swatch-size': sizes._10, },})
const colorPickerSwatchIndicator = stylex.create({ base: { color: colors.white, borderRadius: radii.full, },})
const colorPickerChannelSliderThumb = stylex.create({ base: { borderRadius: radii.full, height: 'var(--thumb-size)', width: 'var(--thumb-size)', borderWidth: '2px', borderColor: colors.white, boxShadow: semanticShadows.sm, transform: 'translate(-50%, -50%)', outlineWidth: { default: null, ':focus-visible': '1px', }, outlineOffset: { default: null, ':focus-visible': '2px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, focusRingOffset: '1px', },})
const colorPickerChannelInput = stylex.create({ base: { width: '100%', minWidth: 'var(--input-height)', outline: 'none', position: 'relative', appearance: 'none', textAlign: 'start', opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, height: 'var(--input-height)', '--focus-color': colorPalette.focusRing, '--error-color': semanticColors.borderError, '--focus-ring-color': { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, borderColor: { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, },})
const colorPickerChannelInputSizes = stylex.create({ '2xs': { borderRadius: semanticRadii.control, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, paddingInline: spacing._2, '--input-height': sizes.controlXs, }, xs: { borderRadius: semanticRadii.control, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, paddingInline: spacing._2, '--input-height': sizes.controlXs, }, sm: { borderRadius: semanticRadii.control, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, paddingInline: spacing._2_5, '--input-height': sizes.controlSm, }, md: { borderRadius: semanticRadii.control, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, paddingInline: spacing._3, '--input-height': sizes.controlMd, }, lg: { borderRadius: semanticRadii.control, fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingInline: spacing._4_5, '--input-height': sizes.controlLg, }, xl: { borderRadius: semanticRadii.control, fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingInline: spacing._6, '--input-height': sizes.controlXl, }, '2xl': { borderRadius: semanticRadii.control, fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingInline: spacing._6, '--input-height': sizes.controlXl, },})
const colorPickerChannelInputVariants = stylex.create({ outline: { backgroundColor: semanticColors.bg, borderWidth: '1px', borderColor: { default: semanticColors.border, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', ':hover': { default: semanticColors.borderEmphasized, ':focus-visible': 'var(--focus-ring-color)', }, }, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, focusRingWidth: 0, }, subtle: { borderWidth: '1px', borderColor: { default: 'transparent', ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, backgroundColor: semanticColors.bgMuted, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, },})
const colorPickerFormatSelect = stylex.create({ base: { fontSize: fontSizes.xs, lineHeight: lineHeights.xs, textTransform: 'uppercase', borderWidth: '1px', minHeight: sizes._6, outlineWidth: { default: null, ':focus': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus': '0px', }, outlineStyle: { default: null, ':focus': 'solid', }, outlineColor: { default: null, ':focus': colorPalette.focusRing, }, borderRadius: semanticRadii.controlMd, },})
const colorPickerTransparencyGrid = stylex.create({ base: { borderRadius: semanticRadii.controlMd, },})
const colorPickerView = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._2, },})
export type ColorPickerSize = keyof typeof colorPickerTriggerSizes
export type ColorPickerVariant = keyof typeof colorPickerTriggerVariants
const colorPickerRecipe = defineSlotRecipe({ slots: { root: { styles: colorPickerRoot }, label: { styles: colorPickerLabel }, valueText: { styles: colorPickerValueText }, control: { styles: colorPickerControl }, swatchTrigger: { styles: colorPickerSwatchTrigger }, trigger: { styles: colorPickerTrigger, sizes: colorPickerTriggerSizes, variants: colorPickerTriggerVariants, }, content: { styles: colorPickerContent }, area: { styles: colorPickerArea, sizes: colorPickerAreaSizes }, areaThumb: { styles: colorPickerAreaThumb }, areaBackground: { styles: colorPickerAreaBackground }, channelSlider: { styles: colorPickerChannelSlider, sizes: colorPickerChannelSliderSizes, }, channelSliderTrack: { styles: colorPickerChannelSliderTrack }, swatchGroup: { styles: colorPickerSwatchGroup }, swatch: { styles: colorPickerSwatch, sizes: colorPickerSwatchSizes }, swatchIndicator: { styles: colorPickerSwatchIndicator }, channelSliderThumb: { styles: colorPickerChannelSliderThumb }, channelInput: { styles: colorPickerChannelInput, sizes: colorPickerChannelInputSizes, variants: colorPickerChannelInputVariants, }, transparencyGrid: { styles: colorPickerTransparencyGrid }, view: { styles: colorPickerView }, }, defaultVariants: { size: "md", variant: "outline" },});
// chrome the generated recipe leaves to the composition layer: unstyled// trigger buttons and the popup panel surfaceconst chrome = stylex.create({ btnReset: { appearance: "none", backgroundColor: "transparent", borderWidth: 0, borderStyle: "none", padding: 0, margin: 0, fontFamily: "inherit", fontSize: "inherit", color: "inherit", cursor: "pointer", textAlign: "inherit", }, panel: { backgroundColor: semanticColors.bgPanel, borderRadius: semanticRadii.panel, boxShadow: semanticShadows.overlay, },});
function CheckIcon(props: React.ComponentProps<"svg">) { return ( <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}> <path d="m3.5 8.5 3 3 6-7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
const { withProvider, withContext } = createSlotRecipeContext(colorPickerRecipe);
export type ColorPickerVariantProps = Partial<SlotRecipeVariants<typeof colorPickerRecipe>>;
const StyledSwatchIndicator = withContext( ArkColorPicker.SwatchIndicator, "swatchIndicator", "ColorPickerSwatchIndicator",);
function SwatchIndicator( props: React.ComponentProps<typeof ArkColorPicker.SwatchIndicator> & StyleOverrideProps,) { const { children, ...rest } = props; return ( <StyledSwatchIndicator {...rest}>{children ?? <CheckIcon />}</StyledSwatchIndicator> );}SwatchIndicator.displayName = "ColorPickerSwatchIndicator";
function Positioner({ children, ...rest }: ArkColorPicker.PositionerProps) { return ( <Portal> <ArkColorPicker.Positioner {...rest}>{children}</ArkColorPicker.Positioner> </Portal> );}
/** * ColorPicker — Ark UI's color-picker machine wearing the generated slot * recipe. `<ColorPicker.Root variant size>` resolves the recipe once; every * part inherits it through context. Content portals through * `ColorPicker.Positioner`; SwatchIndicator ships a default check icon. */export const ColorPicker = { Root: withProvider(ArkColorPicker.Root, "root", "ColorPickerRoot"), Label: withContext(ArkColorPicker.Label, "label", "ColorPickerLabel"), Control: withContext(ArkColorPicker.Control, "control", "ColorPickerControl"), Trigger: withContext( ArkColorPicker.Trigger, (v) => [chrome.btnReset, ...resolveSlotStyles(colorPickerRecipe.slots.trigger, v)], "ColorPickerTrigger", ), ValueText: withContext(ArkColorPicker.ValueText, "valueText", "ColorPickerValueText"), ValueSwatch: withContext(ArkColorPicker.ValueSwatch, "swatch", "ColorPickerValueSwatch"), SwatchGroup: withContext(ArkColorPicker.SwatchGroup, "swatchGroup", "ColorPickerSwatchGroup"), SwatchTrigger: withContext( ArkColorPicker.SwatchTrigger, (v) => [chrome.btnReset, ...resolveSlotStyles(colorPickerRecipe.slots.swatchTrigger, v)], "ColorPickerSwatchTrigger", ), Swatch: withContext(ArkColorPicker.Swatch, "swatch", "ColorPickerSwatch"), SwatchIndicator, Positioner, Content: withContext( ArkColorPicker.Content, (v) => [...resolveSlotStyles(colorPickerRecipe.slots.content, v), chrome.panel], "ColorPickerContent", ), Area: withContext(ArkColorPicker.Area, "area", "ColorPickerArea"), AreaBackground: withContext( ArkColorPicker.AreaBackground, "areaBackground", "ColorPickerAreaBackground", ), AreaThumb: withContext(ArkColorPicker.AreaThumb, "areaThumb", "ColorPickerAreaThumb"), ChannelSlider: withContext( ArkColorPicker.ChannelSlider, "channelSlider", "ColorPickerChannelSlider", ), ChannelSliderTrack: withContext( ArkColorPicker.ChannelSliderTrack, "channelSliderTrack", "ColorPickerChannelSliderTrack", ), ChannelSliderThumb: withContext( ArkColorPicker.ChannelSliderThumb, "channelSliderThumb", "ColorPickerChannelSliderThumb", ), ChannelInput: withContext( ArkColorPicker.ChannelInput, "channelInput", "ColorPickerChannelInput", ), TransparencyGrid: withContext( ArkColorPicker.TransparencyGrid, "transparencyGrid", "ColorPickerTransparencyGrid", ), View: withContext(ArkColorPicker.View, "view", "ColorPickerView"), HiddenInput: ArkColorPicker.HiddenInput, Context: ArkColorPicker.Context,};
export { parseColor } from "@ark-ui/react/color-picker";