rapidUI

Color Picker

Color picker with a saturation/hue area and hex input in a popover.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/color-picker.json

Installs to @/components/ui/color-picker.

Manual installation

Install the dependencies:

pnpm add react-colorful@^5.6.1

Add the components it builds on: input, popover.

Copy the source into your project:

components/ui/color-picker.tsx
'use client'

import * as React from 'react'
import { HexColorPicker } from 'react-colorful'
import { Input } from '@/components/ui/input'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { cn } from '@/lib/utils'

type ColorPickerContextValue = {
	value: string
	setValue: (value: string) => void
	onBlur?: () => void
	disabled?: boolean
}

const ColorPickerContext = React.createContext<ColorPickerContextValue>({
	value: '#000000',
	setValue: () => {},
	disabled: false,
})

type ColorPickerProps = React.ComponentProps<typeof Popover> & {
	value?: string
	onValueChange?: (value: string) => void
	defaultValue?: string

	onBlur?: () => void

	disabled?: boolean
}

function ColorPicker({
	open,

	value: valueProp,
	onValueChange,
	defaultValue,

	onBlur,

	disabled,

	...props
}: ColorPickerProps) {
	const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue ?? '#000000')
	const value = valueProp ?? uncontrolledValue

	const setValue = React.useCallback(
		(next: string) => {
			if (valueProp === undefined) setUncontrolledValue(next)
			onValueChange?.(next)
		},
		[valueProp, onValueChange],
	)

	const context = React.useMemo(() => ({ value, setValue, disabled, onBlur }), [value, setValue, disabled, onBlur])

	return (
		<ColorPickerContext.Provider value={context}>
			<Popover open={disabled ? false : open} {...props} />
		</ColorPickerContext.Provider>
	)
}

function ColorPickerInput({
	disabled: disabledProp,
	onChange,
	onBlur: onBlurProp,
	...props
}: React.ComponentProps<typeof Input>) {
	const { value, setValue, disabled, onBlur } = React.useContext(ColorPickerContext)

	return (
		<Input
			data-slot="color-picker-input"
			value={value}
			disabled={disabledProp ?? disabled}
			onChange={(event) => {
				onChange?.(event)
				if (!event.defaultPrevented) setValue(event.target.value)
			}}
			onBlur={(event) => {
				onBlurProp?.(event)
				if (!event.defaultPrevented) onBlur?.()
			}}
			{...props}
		/>
	)
}

function ColorPickerTrigger({
	className,
	disabled: disabledProp,
	children,
	style,
	...props
}: React.ComponentProps<typeof PopoverTrigger>) {
	const { value, disabled } = React.useContext(ColorPickerContext)

	return (
		<PopoverTrigger
			data-slot="color-picker-trigger"
			className={cn(
				'relative aspect-square size-10 shrink-0 rounded-lg border border-input bg-background bg-clip-padding transition-colors outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
				className,
			)}
			style={{ backgroundColor: value, ...style }}
			disabled={disabledProp ?? disabled}
			aria-disabled={disabledProp ?? disabled}
			{...props}
		>
			<span className="sr-only">Pick a color</span>
			{children}
		</PopoverTrigger>
	)
}

function ColorPickerContent({ className, children, ...props }: React.ComponentProps<typeof PopoverContent>) {
	const { value, setValue } = React.useContext(ColorPickerContext)

	return (
		<PopoverContent data-slot="color-picker-content" className={cn('w-auto p-2', className)} {...props}>
			<HexColorPicker color={value} onChange={setValue} />
			{children}
		</PopoverContent>
	)
}

export { ColorPicker, ColorPickerInput, ColorPickerTrigger, ColorPickerContent }
export type { ColorPickerProps }

A color picker component that allows users to select colors using a color input and a color picker popover.

Features

  • Color input field for direct hex color input
  • Color picker popover with a visual color picker
  • Color preview trigger button
  • Fully accessible
  • Keyboard navigation support
  • Customizable styling

Usage

import { ColorPicker, ColorPickerInput, ColorPickerTrigger, ColorPickerContent } from '@/components/ui/color-picker'

export default function MyComponent() {
  return (
    <ColorPicker defaultValue="#1f6f8b">
      <div className="flex w-full items-center gap-2">
        <ColorPickerInput aria-label="Accent colour" />
        <ColorPickerTrigger />
      </div>
      <ColorPickerContent />
    </ColorPicker>
  )
}

Examples

Default

A labelled colour field: type a hex code or pick a colour from the popover.

With Presets

Extra children of ColorPickerContent render below the picker. Here they offer a palette of preset colours, controlled with value and onValueChange.

Controlled

Several controlled pickers in a list, each with only a trigger. Give each trigger an aria-label that names what it colours.

Disabled

Invalid

Pass aria-invalid to the input and trigger to show the error state, and explain the error in a FieldError.

Props

ColorPicker

  • value: string
    The controlled value of the color picker.

  • defaultValue: string
    The default value of the color picker.

  • onValueChange: (value: string) => void
    Callback fired when the value changes.

  • open: boolean
    The controlled open state of the popover.

  • defaultOpen: boolean
    The default open state of the popover.

  • onOpenChange: (open: boolean, eventDetails) => void
    Callback fired when the open state changes (Base UI Popover).

  • disabled: boolean
    Disables the input and the trigger, and keeps the popover closed.

  • onBlur: () => void
    Called when the input loses focus (handy for form libraries).

ColorPickerInput

Extends the Input component props. Your own onChange/onBlur run first; call event.preventDefault() to stop the picker from updating its value (or calling the root onBlur).

ColorPickerTrigger

Extends the PopoverTrigger component props. A size-8 (h-8, rounded-lg) swatch showing the current colour, with the same focus ring and aria-invalid styles as Input.

ColorPickerContent

Extends the PopoverContent component props. Extra children render below the picker.