HextaUI

ベータVercel の UI SDK 0.1.0-beta.1 上に構築。API は変わる可能性があります。

Color picker

Vercel の UI SDK 上に作ったカラーピッカー。彩度エリア、色相とアルファのスライダー、入力できる CSS カラー、プリセット、スポイトを備えます。

依存関係
  • @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

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

UI SDK は ui-sdk というカスケードレイヤーに少しだけ独自の CSS を追加します。自分のクラスが優先されるよう、グローバルスタイルシートの先頭、Tailwind の import より上でこのレイヤーを宣言してください。

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

値は CSS のカラー文字列です。すべてのパーツが同じ色を編集するので、必要なパーツだけ使ってください。ColorPickerArea と ColorPickerSlider はつまみを自動で描画します。ピッカーには aria-label か、周りのフィールドのラベルで名前を付けます。

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

設定ページやサイドバー向けに、ポップオーバーなしでパーツをその場に表示します。

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 }

channel="alpha" のスライダーは不透明度を編集します。トラックとスウォッチは色の下に市松模様を描くので、透ける色は透けて見えます。

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 }

space="oklch" では ColorPickerAreaCanvas を追加してエリアを描きます。画面の色域外の色は空白のままで、そこへドラッグしたつまみは離すと色域内の最も近い色に収まります。

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 はタブ移動 1 回分で、プリセット間は矢印キーで移動します。値の書き方が違ってもプリセットは選択されたままです。ColorPickerChannelInput は 1 つのチャンネルを数値で編集します。スポイトは画面から色を取れないブラウザでは自動で隠れます。

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 }

スライダーに orientation="vertical" を指定すると、エリアの横に縦に並べられます。

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 はドラッグの各ステップで、onValueCommit は終了時に 1 回実行されるので、保存は後者で行います。createColorPickerController() のコントローラーを使えば、どこからでも色を設定できます。

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

隠し input が name で値を送信し、フォームをリセットすると 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 }

無効なピッカーはフォーカスも変更もできず、何も送信しません。

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 }

readOnly ではつまみにフォーカスでき値も送信されますが、ドラッグ、キー、入力では色は変わりません。

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 はページから文字の方向を読み取るので、エリア、スライダー、矢印キーは左右反転します。input は自分で方向を決めるため、hex 値は左から右に読めます。

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 }
キーアクション
←→↑↓つまみを 1 ステップ動かします。エリアでは左右キーで横に、上下キーで縦に動きます。
Shift + arrow10 ステップ動かします。
Page UpPage Down範囲の 10 分の 1 だけ、エリアでは上下に、スライダーでは軸に沿って動かします。
HomeEnd先頭または末尾へ動かします。
Enterinput のテキストを読み取るか、フォーカス中のプリセットを選びます。
Escinput のテキストを値に戻すか、ポップオーバーを閉じます。

カラーピッカーに当てはまる WAI-ARIA パターンはないため、UI SDK は既存のパターンを組み合わせて作ります。ピッカーはグループです。エリアのつまみと各スライダーのつまみはスライダーで、エリアのつまみは「彩度 76%、明度 96%」のように 2 つのチャンネルを読み上げます。プリセットはリストボックスで、各プリセットとスウォッチは「鮮やかな青」のように色で名前が付きます。

ポップオーバーの例のトリガーは、名前に現在の色を含みます。つまみはモーションが許可されているときだけドラッグ中に大きくなり、市松模様、リング、フォーカス枠は強制カラーモードでシステムカラーに切り替わります。

各パーツは同名の UI SDK パーツをラップし、render やパーツの状態を受け取る関数にもできる className を含むすべての props を受け付けます。

プロパティ型デフォルト
value
string–
defaultValue
string"black"
onValueChangeドラッグの各ステップ、キー操作、入力した色、プリセットのたびに実行されます。
(value: string, details: ChangeDetails) => void–
onValueCommitドラッグ、キー操作、色の入力が終わったときに 1 回実行されます。
(value: string, details: ChangeDetails) => void–
spaceパーツが編集するチャンネル。
"hsv" | "hsl" | "srgb" | "oklch""hsv"
format値の書き方。hsv では hex、それ以外はその色空間の CSS 構文です。
ColorFormat–
gamut
"srgb" | "p3" | false"srgb"
disabled
booleanfalse
readOnly
booleanfalse
required
booleanfalse
name
string–
form
string–
controllercreateColorPickerController() で作成します。
ColorPickerController–
render
ReactElement | (props, state) => ReactElement<div>
属性説明
data-slot="color-picker"CSSでルートを指定します。
data-disabled無効のときに存在します。
data-readonly読み取り専用のときに付与されます。
data-required必須のときに付与されます。
data-out-of-gamutドラッグで色域外の色を指している間、すべてのパーツに付きます。
--color-picker-colorCSS の色。つまみとスウォッチはこの色で塗られます。
プロパティ型デフォルト
x横方向のチャンネル。色空間により彩度、赤、またはクロマです。
ColorChannel–
y縦方向のチャンネル。色空間により明度、輝度、または緑です。
ColorChannel–
children
ReactNode<ColorPickerAreaThumb />
render
ReactElement | (props, state) => ReactElement<div>
属性説明
data-slot="color-picker-area"CSS でエリアを指定するときに使います。
--color-area-backgroundエリアを描くグラデーション。canvas で描く場合は未設定です。
--color-picker-radiusエリアの角の半径。角が同心になるよう ColorPickerContent が設定します。
属性説明
data-slot="color-picker-area-thumb"CSS でつまみを指定するときに使います。
--area-xエリア内のつまみの横位置。0 から 1。
--area-yエリア内のつまみの縦位置。0 から 1。
プロパティ型デフォルト
channel既定は色相、sRGB では青です。
ColorChannel–
orientation
"horizontal" | "vertical""horizontal"
children
ReactNode<ColorPickerSliderThumb />
render
ReactElement | (props, state) => ReactElement<div>
属性説明
data-slot="color-picker-slider"CSS でスライダーを指定するときに使います。
data-channel編集するチャンネル。色相やアルファなど。
data-orientationhorizontal または vertical。
--color-slider-backgroundチャンネルに沿ったグラデーション。
プロパティ型デフォルト
channel編集するチャンネル。人が読む単位で表示します。
ColorChannel–
render
ReactElement | (props, state) => ReactElement<input>
プロパティ型デフォルト
value必須。プリセットの色です。
string–
render
ReactElement | (props, state) => ReactElement<button>
属性説明
data-slot="color-picker-swatch-item"CSS でプリセットを指定するときに使います。
data-selected値がこのプリセットの色である間に付きます。
プロパティ型デフォルト
children
ReactNode<IconColorPicker />
render
ReactElement | (props, state) => ReactElement<button>
属性説明
data-unsupportedブラウザに EyeDropper がない場合に付きます。そのときボタンは隠れます。

ColorPickerPopover、ColorPickerTrigger、ColorPickerContent は UI SDK のポップオーバーをラップします。コンテンツはブラウザのトップレイヤーに開くため、ポータルは不要です。

プロパティ型デフォルト
side
"top" | "bottom" | "left" | "right" | "start" | "end""bottom"
align
"start" | "center" | "end""start"
sideOffset
number6
属性説明
data-slot="popover-content"UI SDK がポップアップに付けます。CSS で指定するときに使います。
data-starting-styleポップアップが表示アニメーション中に付きます。
data-ending-styleポップアップが非表示アニメーション中に付きます。