HextaUI

BetaGebaut auf Vercels UI SDK 0.1.0-beta.1. Die API kann sich ändern.

Color picker

Ein Farbwähler auf Vercels UI SDK mit Sättigungsfläche, Farbton- und Alpha-Slidern, eintippbaren CSS-Farben, Vorgaben und Pipette.

Abhängig von
  • @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

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

UI SDK bringt etwas eigenes CSS in einem Cascade Layer namens ui-sdk mit. Deklariere diesen Layer ganz oben im globalen Stylesheet, über dem Tailwind-Import, damit deine Klassen gewinnen.

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

Der Wert ist ein CSS-Farbstring. Alle Teile ändern dieselbe Farbe, also nutze nur die Teile, die du brauchst. ColorPickerArea und ColorPickerSlider rendern ihren Thumb selbst. Benenne den Wähler mit aria-label oder mit dem Label eines umgebenden Felds.

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

Rendere die Teile direkt, ohne Popover, für eine Einstellungsseite oder eine Seitenleiste.

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 }

Ein Slider mit channel="alpha" ändert die Deckkraft. Spur und Farbfeld zeichnen ein Schachbrett unter die Farbe, sodass eine durchscheinende Farbe auch so aussieht.

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 }

Mit space="oklch" malt ColorPickerAreaCanvas die Fläche. Farben außerhalb des Bildschirm-Gamuts bleiben leer, und ein dorthin gezogener Thumb springt beim Loslassen auf die nächste Farbe im Gamut.

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 ist ein einziger Tab-Stopp mit Pfeiltasten zwischen den Vorgaben, und eine Vorgabe bleibt ausgewählt, egal wie der Wert geschrieben ist. ColorPickerChannelInput ändert einen Kanal als Zahl. Die Pipette blendet sich in Browsern aus, die nicht vom Bildschirm aufnehmen können.

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 }

Setze orientation="vertical" auf einen Slider, um ihn neben die Fläche zu stellen.

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 läuft bei jedem Schritt eines Ziehens und onValueCommit einmal am Ende, also speichere dort. Ein Controller aus createColorPickerController() setzt die Farbe von überall.

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

Ein verstecktes Input sendet den Wert unter name, und ein Formular-Reset kehrt zu defaultValue zurück.

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

Ein deaktivierter Farbwähler lässt sich weder fokussieren noch ändern und sendet nichts.

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 }

Mit readOnly bleiben die Thumbs fokussierbar und der Wert wird gesendet, aber Ziehen, Tasten und Tippen lassen die Farbe in Ruhe.

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 liest die Richtung von der Seite, daher werden Fläche, Slider und Pfeiltasten gespiegelt. Die Inputs wählen ihre eigene Richtung, sodass ein Hex-Wert weiter von links nach rechts läuft.

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 }
TasteAktion
←→↑↓Bewegt einen Thumb um einen Schritt. Auf der Fläche bewegen links und rechts quer, hoch und runter nach oben und unten.
Shift + arrowBewegt um zehn Schritte.
Page UpPage DownBewegt um ein Zehntel des Bereichs, auf der Fläche nach oben oder entlang eines Sliders.
HomeEndSpringt an den Anfang oder das Ende.
EnterÜbernimmt den Text im Input oder wählt die fokussierte Vorgabe.
EscSetzt den Text im Input auf den Wert zurück oder schließt das Popover.

Kein WAI-ARIA-Muster deckt einen Farbwähler ab, deshalb baut UI SDK ihn aus Mustern, die es gibt. Der Wähler ist eine Gruppe. Der Thumb der Fläche und jeder Slider-Thumb sind Slider, und der Thumb der Fläche nennt beide Kanäle, etwa „Sättigung 76 %, Helligkeit 96 %“. Die Vorgaben sind eine Listbox, und jede Vorgabe und das Farbfeld werden nach ihrer Farbe benannt, etwa „kräftiges Blau“.

Der Trigger im Popover-Beispiel nennt die aktuelle Farbe in seinem Namen. Thumbs wachsen beim Ziehen nur, wenn Bewegung erlaubt ist, und Schachbrett, Ringe und Fokusrahmen wechseln im Modus „Erzwungene Farben“ zu Systemfarben.

Jeder Teil umhüllt den gleichnamigen UI-SDK-Teil und nimmt alle seine Props an, auch render und einen className, der eine Funktion des Zustands sein kann.

PropTypStandard
value
string–
defaultValue
string"black"
onValueChangeLäuft bei jedem Schritt eines Ziehens, jedem Tastendruck, jeder eingetippten Farbe oder Vorgabe.
(value: string, details: ChangeDetails) => void–
onValueCommitLäuft einmal, wenn ein Ziehen, ein Tastendruck oder eine eingetippte Farbe endet.
(value: string, details: ChangeDetails) => void–
spaceWelche Kanäle die Teile ändern.
"hsv" | "hsl" | "srgb" | "oklch""hsv"
formatWie der Wert geschrieben wird. Hex bei hsv, sonst die CSS-Syntax des Farbraums.
ColorFormat–
gamut
"srgb" | "p3" | false"srgb"
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllerAus createColorPickerController().
ColorPickerController–
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="color-picker"Das Root in CSS ansprechen.
data-disabledVorhanden, wenn deaktiviert.
data-readonlyVorhanden, wenn schreibgeschützt.
data-requiredVorhanden, wenn erforderlich.
data-out-of-gamutAuf jedem Teil vorhanden, solange ein Ziehen eine Farbe außerhalb des Gamuts hält.
--color-picker-colorDie Farbe als CSS. Thumbs und Farbfelder werden damit gefüllt.
PropTypStandard
xDer Kanal quer. Je nach Farbraum Sättigung, Rot oder Chroma.
ColorChannel–
yDer Kanal nach oben. Je nach Farbraum Helligkeit, Luminanz oder Grün.
ColorChannel–
children
ReactNode<ColorPickerAreaThumb />
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="color-picker-area"Spricht die Fläche in CSS an.
--color-area-backgroundDie Verläufe, die die Fläche zeichnen. Nicht gesetzt, wenn ein Canvas sie malt.
--color-picker-radiusDer Eckenradius der Fläche. ColorPickerContent setzt ihn, damit die Ecken konzentrisch sind.
AttributBeschreibung
data-slot="color-picker-area-thumb"Spricht den Thumb in CSS an.
--area-xWo der Thumb quer auf der Fläche steht, von 0 bis 1.
--area-yWo der Thumb auf der Fläche nach oben steht, von 0 bis 1.
PropTypStandard
channelStandardmäßig der Farbton, in sRGB Blau.
ColorChannel–
orientation
"horizontal" | "vertical""horizontal"
children
ReactNode<ColorPickerSliderThumb />
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="color-picker-slider"Spricht den Slider in CSS an.
data-channelDer Kanal, den er ändert, etwa Farbton oder Alpha.
data-orientationhorizontal oder vertical.
--color-slider-backgroundEin Verlauf durch den Kanal.
PropTypStandard
channelDer zu ändernde Kanal, in den Einheiten, in denen man ihn liest.
ColorChannel–
render
ReactElement | (props, state) => ReactElement<input>
PropTypStandard
valuePflicht. Die Farbe der Vorgabe.
string–
render
ReactElement | (props, state) => ReactElement<button>
AttributBeschreibung
data-slot="color-picker-swatch-item"Spricht Vorgaben in CSS an.
data-selectedVorhanden, solange der Wert die Farbe dieser Vorgabe ist.
PropTypStandard
children
ReactNode<IconColorPicker />
render
ReactElement | (props, state) => ReactElement<button>
AttributBeschreibung
data-unsupportedVorhanden, wenn der Browser kein EyeDropper hat. Der Button wird dann ausgeblendet.

ColorPickerPopover, ColorPickerTrigger und ColorPickerContent umhüllen das UI-SDK-Popover. Der Inhalt öffnet im Top Layer des Browsers und braucht daher kein Portal.

PropTypStandard
side
"top" | "bottom" | "left" | "right" | "start" | "end""bottom"
align
"start" | "center" | "end""start"
sideOffset
number6
AttributBeschreibung
data-slot="popover-content"Von UI SDK am Popup gesetzt. Damit in CSS ansprechen.
data-starting-styleVorhanden, solange das Popup einblendet.
data-ending-styleVorhanden, solange das Popup ausblendet.