Color Picker
A comprehensive color picker with multiple input methods
registry/preskok/ui/preskok-ui/color-picker.tsx
"use client"
import React, { use } from "react"
import { twJoin, twMerge } from "cn"
import { PipetteIcon } from "lucide-react"
import {
ColorPicker as ColorPickerPrimitive,
ColorPickerStateContext,
type ColorPickerProps as ColorPickerPrimitiveProps,
} from "react-aria-components/ColorPicker"
import { parseColor } from "react-stately/Color"
import { Button } from "./button"
import { ColorArea } from "./color-area"
import { ColorField } from "./color-field"
import { ColorSlider } from "./color-slider"
import { ColorSwatch } from "./color-swatch"
import { Description } from "./field"
import { Popover, PopoverContent, type PopoverContentProps } from "./popover"
interface ColorPickerProps
extends
Omit<ColorPickerPrimitiveProps, "children">,
Pick<PopoverContentProps, "placement"> {
label?: string
className?: string
children?: React.ReactNode
showArrow?: boolean
isDisabled?: boolean
description?: string
eyeDropper?: boolean
}
const ColorPicker = ({
showArrow = false,
placement = "bottom start",
label,
isDisabled,
children,
description,
eyeDropper,
className,
...props
}: ColorPickerProps) => {
return (
<div className={twMerge("flex flex-col items-start gap-y-1", className)}>
<ColorPickerPrimitive {...props}>
<Popover>
<Button
isDisabled={isDisabled}
size={label ? "md" : "sq-sm"}
intent="plain"
className={twJoin(
"w-auto px-2.5 *:data-[slot=color-swatch]:-mx-0.5 *:data-[slot=color-swatch]:size-5",
!label && "size-10"
)}
>
<ColorSwatch />
{label && label}
</Button>
<PopoverContent
className="overflow-auto **:data-[slot=color-area]:w-full **:data-[slot=color-slider]:w-full sm:max-w-56 sm:min-w-min sm:**:data-[slot=color-area]:size-56 *:[[role=dialog]]:p-4 sm:*:[[role=dialog]]:p-3"
arrow={showArrow}
placement={placement}
>
<div className="flex flex-col gap-y-1.5">
{children || (
<>
<ColorArea
colorSpace="hsb"
xChannel="saturation"
yChannel="brightness"
/>
<ColorSlider
showOutput={false}
colorSpace="hsb"
channel="hue"
/>
<div className="flex items-center gap-1.5">
{eyeDropper && <EyeDropper />}
<ColorField className="h-9" aria-label="Hex" />
</div>
</>
)}
</div>
</PopoverContent>
</Popover>
</ColorPickerPrimitive>
{description && <Description>{description}</Description>}
</div>
)
}
declare global {
interface Window {
EyeDropper?: new () => { open: () => Promise<{ sRGBHex: string }> }
}
}
const EyeDropper = () => {
const state = use(ColorPickerStateContext)!
if (!window.EyeDropper) {
return "EyeDropper is not supported in your browser."
}
return (
<Button
aria-label="Eye dropper"
size="sq-sm"
intent="outline"
onPress={async () => {
const eyeDropper = window.EyeDropper ? new window.EyeDropper() : null
await eyeDropper?.open().then((result) => {
state.setColor(parseColor(result.sRGBHex))
})
}}
>
<PipetteIcon />
</Button>
)
}
export { ColorPicker, EyeDropper }
export type { ColorPickerProps }
No selection tags yet — empty means no evidence, not lack of support.
DependenciesBundle weight · light
- icons
- lucide-react