HextaUI

BetaHecho sobre el UI SDK 0.1.0-beta.1 de Vercel. Su API puede cambiar.

Color picker

Un selector de color sobre el UI SDK de Vercel, con área de saturación, sliders de tono y alfa, colores CSS escritos a mano, presets y cuentagotas.

Depende de
  • @tabler/icons-react
  • @ui-sdk/core
  • @ui-sdk/react
"use client"

import * as React from "react"

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerContent,
  ColorPickerEyeDropper,
  ColorPickerInput,
  ColorPickerPopover,
  ColorPickerSlider,
  ColorPickerSwatch,
  ColorPickerSwatches,
  ColorPickerSwatchItem,
  ColorPickerTrigger,
} from "@/components/ui/ui-sdk-color-picker"

const presets = [
  "#0f172a",
  "#ef4444",
  "#f97316",
  "#eab308",
  "#22c55e",
  "#14b8a6",
  "#0ea5e9",
  "#6366f1",
  "#a855f7",
  "#ec4899",
]

function UiSdkColorPickerDemo() {
  const [value, setValue] = React.useState("#6366f1")

  return (
    <ColorPicker
      aria-label="Accent color"
      value={value}
      onValueChange={setValue}
    >
      <ColorPickerPopover>
        <ColorPickerTrigger aria-label={`Accent color, ${value}`}>
          <ColorPickerSwatch />
          <span className="font-mono text-xs uppercase tabular-nums">
            {value}
          </span>
        </ColorPickerTrigger>
        <ColorPickerContent>
          <ColorPickerArea />
          <ColorPickerSlider channel="hue" />
          <div className="flex items-center gap-2">
            <ColorPickerInput aria-label="Hex" />
            <ColorPickerEyeDropper aria-label="Pick a color from the screen" />
          </div>
          <ColorPickerSwatches aria-label="Presets">
            {presets.map((preset) => (
              <ColorPickerSwatchItem key={preset} value={preset} />
            ))}
          </ColorPickerSwatches>
        </ColorPickerContent>
      </ColorPickerPopover>
    </ColorPicker>
  )
}

export { UiSdkColorPickerDemo }
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-color-picker.json

Añade el componente, los tokens del tema de HextaUI y los componentes de HextaUI de los que depende.

UI SDK añade un poco de CSS propio en una capa de cascada llamada ui-sdk. Declara esa capa al principio de tu hoja de estilos global, encima del import de Tailwind, para que tus clases ganen.

@layer ui-sdk;

@import "tailwindcss";
import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"
<ColorPicker aria-label="Brand color" defaultValue="#0ea5e9">
  <ColorPickerArea />
  <ColorPickerSlider channel="hue" />
  <ColorPickerSwatch />
  <ColorPickerInput aria-label="Hex" />
</ColorPicker>

El valor es una cadena de color CSS. Todas las partes editan el mismo color, así que usa solo las que necesites. ColorPickerArea y ColorPickerSlider renderizan su thumb por ti. Nombra el selector con aria-label o con la etiqueta de un campo que lo rodee.

ColorPicker
├── ColorPickerArea
│   ├── ColorPickerAreaCanvas
│   └── ColorPickerAreaThumb
├── ColorPickerSlider
│   └── ColorPickerSliderThumb
├── ColorPickerSwatch
├── ColorPickerInput
├── ColorPickerChannelInput
├── ColorPickerEyeDropper
├── ColorPickerSwatches
│   └── ColorPickerSwatchItem
└── ColorPickerPopover
    ├── ColorPickerTrigger
    └── ColorPickerContent

Renderiza las partes en su sitio, sin popover, para una página de ajustes o una barra lateral.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerInline() {
  return (
    <ColorPicker aria-label="Brand color" defaultValue="#0ea5e9">
      <ColorPickerArea />
      <ColorPickerSlider channel="hue" />
      <div className="flex items-center gap-2">
        <ColorPickerSwatch />
        <ColorPickerInput aria-label="Hex" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerInline }

Un slider con channel="alpha" edita la opacidad. Su pista y la muestra dibujan un damero bajo el color, así un color translúcido se ve translúcido.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerAlpha() {
  return (
    <ColorPicker
      aria-label="Overlay color"
      space="hsl"
      defaultValue="hsl(262 83% 58% / 0.6)"
    >
      <ColorPickerArea />
      <ColorPickerSlider channel="hue" />
      <ColorPickerSlider channel="alpha" />
      <div className="flex items-center gap-2">
        <ColorPickerSwatch />
        <ColorPickerInput aria-label="HSL" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerAlpha }

Con space="oklch", añade ColorPickerAreaCanvas para pintar el área. Los colores fuera del gamut de la pantalla quedan vacíos, y un thumb arrastrado allí se asienta en el color más cercano dentro al soltarlo.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerAreaCanvas,
  ColorPickerAreaThumb,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerOklch() {
  return (
    <ColorPicker
      aria-label="Accent"
      space="oklch"
      defaultValue="oklch(0.62 0.19 259)"
    >
      <ColorPickerArea>
        <ColorPickerAreaCanvas />
        <ColorPickerAreaThumb />
      </ColorPickerArea>
      <ColorPickerSlider channel="hue" />
      <div className="flex items-center gap-2">
        <ColorPickerSwatch />
        <ColorPickerInput aria-label="OKLCH" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerOklch }

ColorPickerSwatches es una sola parada de Tab con flechas entre presets, y un preset sigue seleccionado se escriba como se escriba el valor. ColorPickerChannelInput edita un canal como número. El cuentagotas se oculta en navegadores que no pueden tomar colores de la pantalla.

import {
  ColorPicker,
  ColorPickerChannelInput,
  ColorPickerEyeDropper,
  ColorPickerSwatch,
  ColorPickerSwatches,
  ColorPickerSwatchItem,
} from "@/components/ui/ui-sdk-color-picker"

const presets = [
  "#ef4444",
  "#f97316",
  "#eab308",
  "#22c55e",
  "#0ea5e9",
  "#6366f1",
  "#a855f7",
  "#ec4899",
]

const channels = [
  { channel: "red", label: "R" },
  { channel: "green", label: "G" },
  { channel: "blue", label: "B" },
] as const

function UiSdkColorPickerChannels() {
  return (
    <ColorPicker
      aria-label="Label color"
      space="srgb"
      format="hex"
      defaultValue="#0ea5e9"
    >
      <ColorPickerSwatches aria-label="Presets">
        {presets.map((preset) => (
          <ColorPickerSwatchItem key={preset} value={preset} />
        ))}
      </ColorPickerSwatches>
      <div className="flex items-end gap-2">
        <ColorPickerSwatch />
        {channels.map(({ channel, label }) => (
          <label
            key={channel}
            className="flex min-w-0 flex-1 flex-col gap-1 text-xs text-muted-foreground"
          >
            {label}
            <ColorPickerChannelInput channel={channel} />
          </label>
        ))}
        <ColorPickerEyeDropper aria-label="Pick a color from the screen" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerChannels }

Pon orientation="vertical" en un slider para colocarlo junto al área.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerSlider,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerVertical() {
  return (
    <ColorPicker aria-label="Highlight" space="hsl" defaultValue="#f97316">
      <div className="flex gap-3">
        <div className="min-w-0 flex-1">
          <ColorPickerArea />
        </div>
        <ColorPickerSlider channel="hue" orientation="vertical" />
        <ColorPickerSlider channel="alpha" orientation="vertical" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerVertical }

onValueChange se ejecuta en cada paso de un arrastre y onValueCommit una vez al terminar, así que guarda ahí. Un controlador de createColorPickerController() fija el color desde cualquier parte.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  createColorPickerController,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerControlled() {
  const [picker] = React.useState(() => createColorPickerController())
  const [value, setValue] = React.useState("#0ea5e9")
  const [saved, setSaved] = React.useState("#0ea5e9")

  return (
    <div className="flex w-full max-w-64 flex-col gap-3">
      <ColorPicker
        aria-label="Theme color"
        value={value}
        onValueChange={setValue}
        onValueCommit={setSaved}
        controller={picker}
      >
        <ColorPickerArea />
        <ColorPickerSlider channel="hue" />
        <ColorPickerInput aria-label="Hex" />
      </ColorPicker>
      <div className="flex items-center justify-between gap-2 text-sm">
        <p className="text-muted-foreground">
          Saved <span className="font-mono text-foreground">{saved}</span>
        </p>
        <Button
          variant="outline"
          size="sm"
          onClick={() => picker.setValue("#663399")}
        >
          Use purple
        </Button>
      </div>
    </div>
  )
}

export { UiSdkColorPickerControlled }

Un input oculto envía el valor con name, y al reiniciar el formulario vuelve a defaultValue.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerForm() {
  const [submitted, setSubmitted] = React.useState<string | null>(null)

  return (
    <form
      className="flex w-full max-w-64 flex-col gap-3"
      onSubmit={(event) => {
        event.preventDefault()
        setSubmitted(String(new FormData(event.currentTarget).get("brand")))
      }}
      onReset={() => setSubmitted(null)}
    >
      <ColorPicker aria-label="Brand color" name="brand" defaultValue="#f97316">
        <ColorPickerArea />
        <div className="flex items-center gap-2">
          <ColorPickerSwatch />
          <div className="min-w-0 flex-1">
            <ColorPickerSlider channel="hue" />
          </div>
        </div>
      </ColorPicker>
      <div className="flex gap-2">
        <Button type="submit" size="sm">
          Save
        </Button>
        <Button type="reset" variant="outline" size="sm">
          Reset
        </Button>
      </div>
      <p role="status" className="text-sm text-muted-foreground">
        {submitted ? (
          <>
            Submitted brand=
            <span className="font-mono text-foreground">{submitted}</span>
          </>
        ) : (
          "Nothing submitted yet."
        )}
      </p>
    </form>
  )
}

export { UiSdkColorPickerForm }

Un selector deshabilitado no recibe foco ni cambia, y no envía nada.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerDisabled() {
  return (
    <ColorPicker aria-label="Brand color" defaultValue="#0ea5e9" disabled>
      <ColorPickerArea />
      <ColorPickerSlider channel="hue" />
      <ColorPickerInput aria-label="Hex" />
    </ColorPicker>
  )
}

export { UiSdkColorPickerDisabled }

Con readOnly, los thumbs siguen recibiendo foco y el valor se envía, pero arrastrar, las teclas y escribir no cambian el color.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerReadOnly() {
  return (
    <ColorPicker aria-label="Theme color" defaultValue="#22c55e" readOnly>
      <ColorPickerArea />
      <ColorPickerSlider channel="hue" />
      <div className="flex items-center gap-2">
        <ColorPickerSwatch />
        <ColorPickerInput aria-label="Hex" />
      </div>
    </ColorPicker>
  )
}

export { UiSdkColorPickerReadOnly }

UI SDK lee la dirección de la página, así que el área, los sliders y las flechas se invierten. Los inputs eligen su propia dirección, así un valor hex se sigue leyendo de izquierda a derecha.

import {
  ColorPicker,
  ColorPickerArea,
  ColorPickerInput,
  ColorPickerSlider,
  ColorPickerSwatch,
} from "@/components/ui/ui-sdk-color-picker"

function UiSdkColorPickerRtl() {
  return (
    <div dir="rtl" className="flex w-full justify-center">
      <ColorPicker aria-label="لون العلامة" defaultValue="#ec4899">
        <ColorPickerArea />
        <ColorPickerSlider channel="hue" />
        <div className="flex items-center gap-2">
          <ColorPickerSwatch />
          <ColorPickerInput aria-label="سداسي عشري" />
        </div>
      </ColorPicker>
    </div>
  )
}

export { UiSdkColorPickerRtl }
KeyAcción
←→↑↓Mueve un thumb un paso. En el área, izquierda y derecha mueven en horizontal, arriba y abajo en vertical.
Shift + arrowMueve diez pasos.
Page UpPage DownMueve una décima parte del rango, hacia arriba en el área o a lo largo de un slider.
HomeEndMueve al inicio o al final.
EnterLee el texto del input, o elige el preset con foco.
EscDevuelve el texto del input al valor, o cierra el popover.

Ningún patrón WAI-ARIA cubre un selector de color, así que UI SDK lo arma con los que sí existen. El selector es un grupo. El thumb del área y cada thumb de slider son sliders, y el del área nombra sus dos canales, como “Saturación 76%, Brillo 96%”. Los presets son un listbox, y cada preset y la muestra se nombran por su color, como “azul vivo”.

El trigger del ejemplo con popover dice el color actual en su nombre. Los thumbs crecen al arrastrar solo si se permite el movimiento, y el damero, los anillos y los contornos de foco pasan a colores del sistema en el modo de colores forzados.

Cada parte envuelve la parte de UI SDK del mismo nombre y acepta todas sus props, incluidas render y un className que puede ser una función del estado de la parte.

PropTipoPredeterminado
value
string–
defaultValue
string"black"
onValueChangeSe ejecuta en cada paso de un arrastre, una tecla, un color escrito o un preset.
(value: string, details: ChangeDetails) => void–
onValueCommitSe ejecuta una vez cuando termina un arrastre, una tecla o un color escrito.
(value: string, details: ChangeDetails) => void–
spaceQué canales editan las partes.
"hsv" | "hsl" | "srgb" | "oklch""hsv"
formatCómo se escribe el valor. Hex para hsv; si no, la sintaxis CSS propia del espacio.
ColorFormat–
gamut
"srgb" | "p3" | false"srgb"
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerDe createColorPickerController().
ColorPickerController–
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="color-picker"Apunta a la raíz en CSS.
data-disabledPresente cuando está deshabilitado.
data-readonlyPresente cuando es de solo lectura.
data-requiredPresente cuando es obligatorio.
data-out-of-gamutPresente en cada parte mientras un arrastre deja un color fuera del gamut.
--color-picker-colorEl color como CSS. Los thumbs y las muestras se rellenan con él.
PropTipoPredeterminado
xEl canal horizontal. Saturación, rojo o croma, según el espacio.
ColorChannel–
yEl canal vertical. Brillo, luminosidad o verde, según el espacio.
ColorChannel–
children
ReactNode<ColorPickerAreaThumb />
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="color-picker-area"Para apuntar al área en CSS.
--color-area-backgroundLos degradados que dibujan el área. Sin valor cuando la pinta un canvas.
--color-picker-radiusEl radio de las esquinas del área. Lo fija ColorPickerContent para que las esquinas sean concéntricas.
AtributoDescripción
data-slot="color-picker-area-thumb"Para apuntar al thumb en CSS.
--area-xLa posición horizontal del thumb en el área, de 0 a 1.
--area-yLa posición vertical del thumb en el área, de 0 a 1.
PropTipoPredeterminado
channelPor defecto, el tono, o el azul en sRGB.
ColorChannel–
orientation
"horizontal" | "vertical""horizontal"
children
ReactNode<ColorPickerSliderThumb />
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescripción
data-slot="color-picker-slider"Para apuntar al slider en CSS.
data-channelEl canal que edita, como el tono o el alfa.
data-orientationhorizontal o vertical.
--color-slider-backgroundUn degradado a lo largo del canal.
PropTipoPredeterminado
channelEl canal que se edita, en las unidades en que se lee.
ColorChannel–
render
ReactElement | (props, state) => ReactElement<input>
PropTipoPredeterminado
valueObligatorio. El color del preset.
string–
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescripción
data-slot="color-picker-swatch-item"Para apuntar a los presets en CSS.
data-selectedPresente mientras el valor es el color de este preset.
PropTipoPredeterminado
children
ReactNode<IconColorPicker />
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescripción
data-unsupportedPresente donde el navegador no tiene EyeDropper. El botón se oculta entonces.

ColorPickerPopover, ColorPickerTrigger y ColorPickerContent envuelven el popover de UI SDK. El contenido se abre en la top layer del navegador, así que no necesita portal.

PropTipoPredeterminado
side
"top" | "bottom" | "left" | "right" | "start" | "end""bottom"
align
"start" | "center" | "end""start"
sideOffset
number6
AtributoDescripción
data-slot="popover-content"UI SDK lo pone en el popup. Úsalo en CSS.
data-starting-stylePresente mientras el popup se anima al entrar.
data-ending-stylePresente mientras el popup se anima al salir.