HextaUI

BetaFeito sobre o UI SDK 0.1.0-beta.1 da Vercel. A API pode mudar.

Color picker

Um seletor de cor sobre o UI SDK da Vercel, com área de saturação, sliders de matiz e alfa, cores CSS digitadas, presets e conta-gotas.

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

Adiciona o componente, os tokens de tema do HextaUI e quaisquer componentes do HextaUI dos quais ele depende.

O UI SDK adiciona um pouco de CSS próprio em uma camada de cascata chamada ui-sdk. Declare essa camada no topo da sua folha de estilos global, acima do import do Tailwind, para que suas classes vençam.

@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>

O valor é uma string de cor CSS. Todas as partes editam a mesma cor, então use só as que precisar. ColorPickerArea e ColorPickerSlider renderizam o thumb para você. Nomeie o seletor com aria-label ou com o rótulo de um campo ao redor.

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

Renderize as partes no lugar, sem popover, para uma página de configurações ou uma 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 }

Um slider com channel="alpha" edita a opacidade. A trilha e a amostra desenham um xadrez sob a cor, então uma cor translúcida parece translúcida.

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 }

Com space="oklch", adicione ColorPickerAreaCanvas para pintar a área. Cores fora do gamut da tela ficam vazias, e um thumb arrastado até lá se ajusta à cor mais próxima dentro ao soltar.

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 é uma só parada de Tab com setas entre os presets, e um preset continua selecionado não importa como o valor esteja escrito. ColorPickerChannelInput edita um canal como número. O conta-gotas se oculta em navegadores que não capturam cores da tela.

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 }

Defina orientation="vertical" em um slider para colocá-lo ao lado da á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 executa a cada passo de um arraste e onValueCommit uma vez ao final, então salve ali. Um controlador de createColorPickerController() define a cor de qualquer lugar.

"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 }

Um input oculto envia o valor em name, e o reset do formulário volta para 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 }

Um seletor desativado não recebe foco nem muda, e não envia 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 }

Com readOnly, os thumbs continuam recebendo foco e o valor é enviado, mas arrastar, teclas e digitação não mudam a cor.

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 }

O UI SDK lê a direção da página, então a área, os sliders e as setas se espelham. Os inputs escolhem a própria direção, então um valor hex continua sendo lido da esquerda para a direita.

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 }
TeclaAção
←→↑↓Move um thumb um passo. Na área, esquerda e direita movem na horizontal, cima e baixo na vertical.
Shift + arrowMove dez passos.
Page UpPage DownMove um décimo do intervalo, para cima na área ou ao longo de um slider.
HomeEndMove para o início ou o fim.
EnterLê o texto do input, ou escolhe o preset com foco.
EscVolta o texto do input para o valor, ou fecha o popover.

Nenhum padrão WAI-ARIA cobre um seletor de cor, então o UI SDK o monta com os que existem. O seletor é um grupo. O thumb da área e cada thumb de slider são sliders, e o da área nomeia seus dois canais, como “Saturação 76%, Brilho 96%”. Os presets são um listbox, e cada preset e a amostra são nomeados pela cor, como “azul vivo”.

O trigger do exemplo com popover diz a cor atual no nome. Os thumbs crescem ao arrastar só quando o movimento é permitido, e o xadrez, os anéis e os contornos de foco passam a cores do sistema no modo de cores forçadas.

Cada parte envolve a parte do UI SDK de mesmo nome e aceita todas as suas props, incluindo render e um className que pode ser uma função do estado da parte.

PropTipoPadrão
value
string–
defaultValue
string"black"
onValueChangeExecuta a cada passo de um arraste, tecla, cor digitada ou preset.
(value: string, details: ChangeDetails) => void–
onValueCommitExecuta uma vez quando um arraste, tecla ou cor digitada termina.
(value: string, details: ChangeDetails) => void–
spaceQuais canais as partes editam.
"hsv" | "hsl" | "srgb" | "oklch""hsv"
formatComo o valor é escrito. Hex para hsv; caso contrário, a sintaxe CSS do próprio espaço.
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>
AtributoDescrição
data-slot="color-picker"Selecione a raiz no CSS.
data-disabledPresente quando desabilitado.
data-readonlyPresente quando somente leitura.
data-requiredPresente quando obrigatório.
data-out-of-gamutPresente em cada parte enquanto um arraste mantém uma cor fora do gamut.
--color-picker-colorA cor como CSS. Thumbs e amostras são preenchidos com ela.
PropTipoPadrão
xO canal na horizontal. Saturação, vermelho ou croma, conforme o espaço.
ColorChannel–
yO canal na vertical. Brilho, luminosidade ou verde, conforme o espaço.
ColorChannel–
children
ReactNode<ColorPickerAreaThumb />
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="color-picker-area"Para selecionar a área no CSS.
--color-area-backgroundOs gradientes que desenham a área. Sem valor quando um canvas a pinta.
--color-picker-radiusO raio dos cantos da área. Definido por ColorPickerContent para que os cantos sejam concêntricos.
AtributoDescrição
data-slot="color-picker-area-thumb"Para selecionar o thumb no CSS.
--area-xA posição horizontal do thumb na área, de 0 a 1.
--area-yA posição vertical do thumb na área, de 0 a 1.
PropTipoPadrão
channelPor padrão, o matiz, ou o azul em sRGB.
ColorChannel–
orientation
"horizontal" | "vertical""horizontal"
children
ReactNode<ColorPickerSliderThumb />
render
ReactElement | (props, state) => ReactElement<div>
AtributoDescrição
data-slot="color-picker-slider"Para selecionar o slider no CSS.
data-channelO canal que edita, como matiz ou alfa.
data-orientationhorizontal ou vertical.
--color-slider-backgroundUm gradiente ao longo do canal.
PropTipoPadrão
channelO canal a editar, nas unidades em que as pessoas o leem.
ColorChannel–
render
ReactElement | (props, state) => ReactElement<input>
PropTipoPadrão
valueObrigatório. A cor do preset.
string–
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescrição
data-slot="color-picker-swatch-item"Para selecionar os presets no CSS.
data-selectedPresente enquanto o valor é a cor deste preset.
PropTipoPadrão
children
ReactNode<IconColorPicker />
render
ReactElement | (props, state) => ReactElement<button>
AtributoDescrição
data-unsupportedPresente onde o navegador não tem EyeDropper. O botão fica oculto nesse caso.

ColorPickerPopover, ColorPickerTrigger e ColorPickerContent envolvem o popover do UI SDK. O conteúdo abre na top layer do navegador, então não precisa de portal.

PropTipoPadrão
side
"top" | "bottom" | "left" | "right" | "start" | "end""bottom"
align
"start" | "center" | "end""start"
sideOffset
number6
AtributoDescrição
data-slot="popover-content"Definido pelo UI SDK no popup. Use-o no CSS.
data-starting-stylePresente enquanto o popup anima a entrada.
data-ending-stylePresente enquanto o popup anima a saída.