shshadcn-htmx

Components

Color Picker

A native <input type="color"> styled as a shadcn swatch, with an optional live hex readout. The browser supplies the entire picker UI and guarantees the value is a valid CSS color — we only restyle the swatch and never parse colors ourselves.

Installation

One file per stack. Use the shadcn CLI for JSX, or copy the source for your template engine.

1. Install via the shadcn CLI

npx shadcn@latest add http://localhost/r/color-picker.json

2. Use it

components/ui/color-picker.tsx
import { ColorPicker } from "@/components/ui/color-picker"

<ColorPicker name="brand" value="#e66465" ariaLabel="Brand color" />
Or copy the source manually
components/ui/color-picker.tsx
/** @jsxImportSource hono/jsx */
import { cn, type ClassValue } from "@/registry/lib/cn"

// Color Picker — shadcn-htmx, htmx v4 + Tailwind v4.
//
// Built on native <input type="color">. The browser owns the entire picker
// UI (a platform color dialog or a validating text field) and guarantees the
// value is a valid CSS color — we never reimplement any of that. shadcn/ui has
// no color-picker; we mirror the Input anatomy
// (repos/shadcn-ui/apps/v4/registry/new-york-v4/ui/input.tsx) and lean on the
// platform exactly like registry/ui/slider.tsx does for <input type=range>.
//
// What the platform gives us, per MDN:
//   - the whole color-selection UI + value validation; an invalid value is
//     coerced and :invalid is applied (we never parse colors ourselves)
//   - `value` is a CSS <color>; default is #000000 when omitted/invalid
//   - `alpha` (boolean) lets the user edit the alpha channel
//   - `colorspace` ("limited-srgb" | "display-p3") hints the picker + gamut
//   - `input` fires continuously as the color changes, `change` on dismiss
//   - supported common attributes: autocomplete, list (a <datalist> of swatches)
//   - it has NO implicit ARIA role, so a visible <label for> or ariaLabel is
//     required for an accessible name
//   See repos/mdn/files/en-us/web/html/reference/elements/input/color/index.md
//       (Value:54, alpha/colorspace:63-69, events:96, common attrs:219,
//        validation:121, Implicit ARIA Role: none:244)
//
// We hide the browser's default swatch chrome via Tailwind v4's pseudo-element
// selectors ([&::-webkit-color-swatch] / [&::-moz-color-swatch]) so the control
// reads as one rounded shadcn swatch — the same -webkit-/-moz- pairing the
// Slider uses for its thumb. Both engines need separate rules.
//
// JS budget: none for the swatch itself (it is a real <input>). The optional
// `showValue` hex readout is synced by a 6-line handler in public/site.js keyed
// on data-slot="color-picker"; with showValue={false} it is zero-JS.

export type ColorSpace = "limited-srgb" | "display-p3"

const swatchBase =
  // The native <input type=color> styled as a single rounded swatch button.
  "size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs outline-none transition-[color,box-shadow] " +
  "dark:bg-input/30 " +
  "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " +
  "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " +
  "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " +
  "[&.htmx-request]:opacity-70 " +
  // Strip the platform swatch chrome so only our rounded fill shows.
  "[&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-sm [&::-webkit-color-swatch]:border-0 " +
  "[&::-moz-color-swatch]:rounded-sm [&::-moz-color-swatch]:border-0"

const valueText =
  "font-mono text-sm tabular-nums text-muted-foreground uppercase select-none"

export function colorPickerClasses(opts?: { class?: ClassValue }): string {
  return cn(swatchBase, opts?.class)
}

type ColorPickerProps = {
  id?: string
  name?: string
  // A CSS <color>. Defaults to #000000 if omitted or invalid (per MDN).
  value?: string
  required?: boolean
  disabled?: boolean
  autofocus?: boolean
  form?: string
  // Id of a <datalist> of preset color swatches the browser offers.
  list?: string
  autocomplete?: string

  // Let the user edit the alpha channel (experimental; ignored where unsupported).
  alpha?: boolean
  // Hint the picker's color space + gamut.
  colorspace?: ColorSpace

  // Render the hex value next to the swatch as a live <output>. Defaults to
  // true. With false the component is a bare swatch (zero JS).
  showValue?: boolean

  // ARIA / labelling. <input type=color> has no implicit role, so a visible
  // <label for> or one of these is required for an accessible name.
  ariaLabel?: string
  ariaLabelledby?: string
  ariaDescribedby?: string
  ariaInvalid?: boolean | "grammar" | "spelling"
  ariaRequired?: boolean

  class?: ClassValue

  // htmx v4 passthrough — fires on the input's change event by default (when
  // the picker is dismissed). Use hx-trigger="input" to push every adjustment.
  // See repos/htmx/www/reference.md.
  [key: `hx-${string}`]: any
  [key: `data-${string}`]: any
}

export function ColorPicker(props: ColorPickerProps) {
  const {
    id,
    name,
    value = "#000000",
    required,
    disabled,
    autofocus,
    form,
    list,
    autocomplete,
    alpha,
    colorspace,
    showValue = true,
    ariaLabel,
    ariaLabelledby,
    ariaDescribedby,
    ariaInvalid,
    ariaRequired,
    class: className,
    ...rest
  } = props

  const swatch = (
    <input
      type="color"
      id={id}
      name={name}
      value={value}
      required={required}
      disabled={disabled}
      autofocus={autofocus}
      form={form}
      list={list}
      autocomplete={autocomplete}
      alpha={alpha ? "" : undefined}
      colorspace={colorspace}
      aria-label={ariaLabel}
      aria-labelledby={ariaLabelledby}
      aria-describedby={ariaDescribedby}
      aria-invalid={ariaInvalid === undefined ? undefined : String(ariaInvalid)}
      aria-required={ariaRequired === undefined ? undefined : String(ariaRequired)}
      data-slot={showValue ? "color-picker-swatch" : "color-picker"}
      class={cn(swatchBase, !showValue && className)}
      {...(showValue ? {} : rest)}
    />
  )

  if (!showValue) return swatch

  // showValue on: a flex shell pairs the native swatch with a live hex
  // <output> that public/site.js mirrors from the input's value (keyed on
  // data-slot="color-picker"). The output is decorative (aria-hidden) — the
  // input is the labelled control and the source of truth for forms + AT.
  return (
    <span
      data-slot="color-picker"
      data-disabled={disabled ? "true" : undefined}
      class={cn("inline-flex items-center gap-2", disabled && "opacity-50", className)}
      {...rest}
    >
      {swatch}
      <output data-slot="color-picker-value" aria-hidden="true" class={valueText}>
        {value}
      </output>
    </span>
  )
}

1. Save the file

Copy color-picker.html into templates/components/.

2. Use it

templates/components/color-picker.html
{% from "components/color-picker.html" import color_picker %}

{{ color_picker(name="brand", value="#e66465", aria_label="Brand color") }}
View source
templates/components/color-picker.html
{# Color Picker macro — shadcn-htmx, htmx v4 + Tailwind v4.
   Mirrors registry/ui/color-picker.tsx for Python/Flask/FastAPI/Django/Jinja2.

   The control is a native <input type="color"> — the browser owns the whole
   picker UI and validates the CSS color value. <input type=color> has no
   implicit ARIA role, so pass a visible <label for> or aria_label. The optional
   hex <output> is mirrored from the input's value by the shared handler in
   public/site.js (keyed on data-slot="color-picker"). With show_value=false the
   component is a bare swatch (zero JS).

   Usage:
       {% from "components/color-picker.html" import color_picker %}
       {{ color_picker(name="brand", value="#e66465", aria_label="Brand color") }}
       {{ color_picker(name="bg", value="#1d4ed8", show_value=false) }}

   See repos/mdn/files/en-us/web/html/reference/elements/input/color/index.md. #}

{% macro color_picker(
    id=none,
    name=none,
    value="#000000",
    required=false,
    disabled=false,
    autofocus=false,
    form=none,
    list=none,
    autocomplete=none,
    alpha=false,
    colorspace=none,
    show_value=true,
    aria_label=none,
    aria_labelledby=none,
    aria_describedby=none,
    aria_invalid=none,
    aria_required=none,
    extra_class="",
    **attrs
) %}
{%- set swatch_base -%}
size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&.htmx-request]:opacity-70 [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-sm [&::-webkit-color-swatch]:border-0 [&::-moz-color-swatch]:rounded-sm [&::-moz-color-swatch]:border-0
{%- endset -%}
{%- set value_text -%}
font-mono text-sm tabular-nums text-muted-foreground uppercase select-none
{%- endset -%}
{%- macro swatch(slot) -%}
<input type="color"
       {%- if id %} id="{{ id }}"{% endif %}
       {%- if name %} name="{{ name }}"{% endif %}
       value="{{ value }}"
       {%- if required %} required{% endif %}
       {%- if disabled %} disabled{% endif %}
       {%- if autofocus %} autofocus{% endif %}
       {%- if form %} form="{{ form }}"{% endif %}
       {%- if list %} list="{{ list }}"{% endif %}
       {%- if autocomplete %} autocomplete="{{ autocomplete }}"{% endif %}
       {%- if alpha %} alpha{% endif %}
       {%- if colorspace %} colorspace="{{ colorspace }}"{% endif %}
       {%- if aria_label %} aria-label="{{ aria_label }}"{% endif %}
       {%- if aria_labelledby %} aria-labelledby="{{ aria_labelledby }}"{% endif %}
       {%- if aria_describedby %} aria-describedby="{{ aria_describedby }}"{% endif %}
       {%- if aria_invalid is not none %} aria-invalid="{{ aria_invalid|string|lower }}"{% endif %}
       {%- if aria_required is not none %} aria-required="{{ aria_required|string|lower }}"{% endif %}
       data-slot="{{ slot }}"
       class="{{ swatch_base }}{% if not show_value %} {{ extra_class }}{% endif %}"
       {%- if not show_value %}{% for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor %}{% endif -%}
>
{%- endmacro -%}
{%- if show_value -%}
<span data-slot="color-picker"
      {%- if disabled %} data-disabled="true"{% endif %}
      class="inline-flex items-center gap-2{% if disabled %} opacity-50{% endif %} {{ extra_class }}"
      {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}="{{ v }}"{% endfor -%}
>
  {{ swatch("color-picker-swatch") }}
  <output data-slot="color-picker-value" aria-hidden="true" class="{{ value_text }}">{{ value }}</output>
</span>
{%- else -%}
{{ swatch("color-picker") }}
{%- endif -%}
{% endmacro %}

1. Save the file

Add color-picker.tmpl alongside your templates.

2. Use it

components/color-picker.tmpl
tpl.ExecuteTemplate(w, "color-picker", map[string]any{
    "Name": "brand", "Value": "#e66465", "AriaLabel": "Brand color",
})
View source
components/color-picker.tmpl
{{/*
  Color Picker template — shadcn-htmx, htmx v4 + Tailwind v4.
  Mirrors registry/ui/color-picker.tsx for Go projects using html/template.

  The control is a native <input type="color">: the browser owns the whole
  picker UI and validates the CSS color value. <input type=color> has no
  implicit ARIA role, so pass a visible <label for> or AriaLabel. The optional
  hex <output> is mirrored from the input's value by the shared handler in
  public/site.js (keyed on data-slot="color-picker"). With NoValue, the
  component is a bare swatch (zero JS).

  Args (via dict):
      ID, Name, Value              string  // Value is a CSS <color>, default #000000
      Required, Disabled, Autofocus bool
      Form, List, Autocomplete     string
      Alpha                        bool    // allow editing the alpha channel
      Colorspace                   string  // "limited-srgb" | "display-p3"
      NoValue                      bool    // omit the hex readout (bare swatch)
      AriaLabel, AriaLabelledby    string
      AriaDescribedby              string
      AriaInvalid, AriaRequired    string  // "true" | "false"
      Attrs                        map[string]string // hx-*, data-*, …

      {{template "color-picker" (dict "Name" "brand" "Value" "#e66465" "AriaLabel" "Brand color")}}

  See repos/mdn/files/en-us/web/html/reference/elements/input/color/index.md.
*/}}

{{define "color-picker"}}
{{- $showValue := not .NoValue -}}
{{- $value := or .Value "#000000" -}}
{{- $swatchBase := "size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&.htmx-request]:opacity-70 [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-sm [&::-webkit-color-swatch]:border-0 [&::-moz-color-swatch]:rounded-sm [&::-moz-color-swatch]:border-0" -}}
{{- $valueText := "font-mono text-sm tabular-nums text-muted-foreground uppercase select-none" -}}
{{- if $showValue -}}
<span data-slot="color-picker"
      {{- if .Disabled}} data-disabled="true"{{end}}
      class="inline-flex items-center gap-2{{if .Disabled}} opacity-50{{end}}"
      {{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>
  <input type="color"
       {{- if .ID}} id="{{.ID}}"{{end}}
       {{- if .Name}} name="{{.Name}}"{{end}}
       value="{{$value}}"
       {{- if .Required}} required{{end}}
       {{- if .Disabled}} disabled{{end}}
       {{- if .Autofocus}} autofocus{{end}}
       {{- if .Form}} form="{{.Form}}"{{end}}
       {{- if .List}} list="{{.List}}"{{end}}
       {{- if .Autocomplete}} autocomplete="{{.Autocomplete}}"{{end}}
       {{- if .Alpha}} alpha{{end}}
       {{- if .Colorspace}} colorspace="{{.Colorspace}}"{{end}}
       {{- if .AriaLabel}} aria-label="{{.AriaLabel}}"{{end}}
       {{- if .AriaLabelledby}} aria-labelledby="{{.AriaLabelledby}}"{{end}}
       {{- if .AriaDescribedby}} aria-describedby="{{.AriaDescribedby}}"{{end}}
       {{- if .AriaInvalid}} aria-invalid="{{.AriaInvalid}}"{{end}}
       {{- if .AriaRequired}} aria-required="{{.AriaRequired}}"{{end}}
       data-slot="color-picker-swatch"
       class="{{$swatchBase}}">
  <output data-slot="color-picker-value" aria-hidden="true" class="{{$valueText}}">{{$value}}</output>
</span>
{{- else -}}
<input type="color"
     {{- if .ID}} id="{{.ID}}"{{end}}
     {{- if .Name}} name="{{.Name}}"{{end}}
     value="{{$value}}"
     {{- if .Required}} required{{end}}
     {{- if .Disabled}} disabled{{end}}
     {{- if .Autofocus}} autofocus{{end}}
     {{- if .Form}} form="{{.Form}}"{{end}}
     {{- if .List}} list="{{.List}}"{{end}}
     {{- if .Autocomplete}} autocomplete="{{.Autocomplete}}"{{end}}
     {{- if .Alpha}} alpha{{end}}
     {{- if .Colorspace}} colorspace="{{.Colorspace}}"{{end}}
     {{- if .AriaLabel}} aria-label="{{.AriaLabel}}"{{end}}
     {{- if .AriaLabelledby}} aria-labelledby="{{.AriaLabelledby}}"{{end}}
     {{- if .AriaDescribedby}} aria-describedby="{{.AriaDescribedby}}"{{end}}
     {{- if .AriaInvalid}} aria-invalid="{{.AriaInvalid}}"{{end}}
     {{- if .AriaRequired}} aria-required="{{.AriaRequired}}"{{end}}
     data-slot="color-picker"
     class="{{$swatchBase}}"
     {{- range $k, $v := .Attrs}} {{$k}}="{{$v}}"{{end -}}
>
{{- end -}}
{{end}}

1. Save the file

Drop color_picker.ex into lib/my_app_web/components/.

2. Use it

lib/my_app_web/components/color_picker.ex
<.color_picker name="brand" value="#e66465" aria-label="Brand color" />
View source
lib/my_app_web/components/color_picker.ex
defmodule ShadcnHtmx.Components.ColorPicker do
  @moduledoc """
  Color Picker — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.

  Mirrors registry/ui/color-picker.tsx. The control is a native
  `<input type="color">`, so the browser owns the whole picker UI and validates
  the CSS color value. `<input type=color>` has no implicit ARIA role, so pass a
  visible `<label for>` or `aria-label`. The optional hex `<output>` is mirrored
  from the input's value by the shared handler in public/site.js (keyed on
  `data-slot="color-picker"`). With `show_value={false}` it is a bare swatch
  (zero JS).

  ## Examples

      <.color_picker name="brand" value="#e66465" aria-label="Brand color" />
      <.color_picker name="bg" value="#1d4ed8" show_value={false} />

  See repos/mdn/files/en-us/web/html/reference/elements/input/color/index.md.
  """

  use Phoenix.Component

  @swatch_base "size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs " <>
                 "outline-none transition-[color,box-shadow] dark:bg-input/30 " <>
                 "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " <>
                 "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 " <>
                 "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " <>
                 "[&.htmx-request]:opacity-70 " <>
                 "[&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-sm [&::-webkit-color-swatch]:border-0 " <>
                 "[&::-moz-color-swatch]:rounded-sm [&::-moz-color-swatch]:border-0"

  @value_text "font-mono text-sm tabular-nums text-muted-foreground uppercase select-none"

  attr :value, :string, default: "#000000"
  attr :show_value, :boolean, default: true
  attr :disabled, :boolean, default: false
  attr :class, :string, default: nil

  attr :rest, :global,
    include:
      ~w(hx-get hx-post hx-put hx-patch hx-delete hx-target hx-swap hx-trigger hx-indicator hx-vals hx-include
         id name required autofocus form list autocomplete alpha colorspace
         aria-label aria-labelledby aria-describedby aria-invalid aria-required)

  def color_picker(assigns) do
    assigns =
      assigns
      |> assign(:swatch_base, @swatch_base)
      |> assign(:value_text, @value_text)

    ~H"""
    <span
      :if={@show_value}
      data-slot="color-picker"
      data-disabled={@disabled && "true"}
      class={["inline-flex items-center gap-2", @disabled && "opacity-50", @class]}
    >
      <input
        type="color"
        value={@value}
        disabled={@disabled}
        data-slot="color-picker-swatch"
        class={@swatch_base}
        {@rest}
      />
      <output data-slot="color-picker-value" aria-hidden="true" class={@value_text}>{@value}</output>
    </span>
    <input
      :if={!@show_value}
      type="color"
      value={@value}
      disabled={@disabled}
      data-slot="color-picker"
      class={[@swatch_base, @class]}
      {@rest}
    />
    """
  end
end

1. Save the file

Paste the markup; relies only on theme tokens. The inline <script> mirrors the hex readout standalone.

2. Use it

snippets/color-picker.html
<span data-slot="color-picker" class="inline-flex items-center gap-2">
  <input type="color" name="brand" value="#e66465" aria-label="Brand color"
         data-slot="color-picker-swatch" class="size-9 … rounded-md border …">
  <output data-slot="color-picker-value" aria-hidden="true" class="font-mono …">#e66465</output>
</span>
View source
snippets/color-picker.html
<!--
  shadcn-htmx — raw Color Picker snippets.

  Mirrors registry/ui/color-picker.tsx. Drop onto any page that loads Tailwind
  CSS v4 and the shadcn theme variables (background, foreground, input, ring,
  destructive, muted-foreground). See app/styles/input.css for defaults.

  The control is a native <input type="color"> — the browser owns the whole
  picker UI and validates the CSS color value (see MDN). It has no implicit ARIA
  role, so pair it with a visible <label for> or aria-label. The hex <output>
  next to the swatch is mirrored from the input's value by the inline boot
  script below (data-slot="color-picker"). In the docs app the same handler
  lives in public/site.js, so you can delete the script there. The bare-swatch
  variant has no JS at all.

  SWATCH BASE (native <input type=color> styled as one rounded swatch; strips
  the platform swatch chrome via -webkit-/-moz- pseudo-elements):
    size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent
    p-1 shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30
    focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50
    aria-invalid:border-destructive aria-invalid:ring-destructive/20
    dark:aria-invalid:ring-destructive/40
    disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50
    [&.htmx-request]:opacity-70
    [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-sm
    [&::-webkit-color-swatch]:border-0 [&::-moz-color-swatch]:rounded-sm
    [&::-moz-color-swatch]:border-0
-->

<!-- ─── With hex readout (default) ──────────────────────────────────── -->
<span data-slot="color-picker" class="inline-flex items-center gap-2">
  <input type="color" name="brand" value="#e66465" aria-label="Brand color" data-slot="color-picker-swatch"
    class="size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&.htmx-request]:opacity-70 [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-sm [&::-webkit-color-swatch]:border-0 [&::-moz-color-swatch]:rounded-sm [&::-moz-color-swatch]:border-0">
  <output data-slot="color-picker-value" aria-hidden="true" class="font-mono text-sm tabular-nums text-muted-foreground uppercase select-none">#e66465</output>
</span>

<!-- ─── Bare swatch, no readout — zero JS ───────────────────────────── -->
<input type="color" name="bg" value="#1d4ed8" aria-label="Background color" data-slot="color-picker"
  class="size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&.htmx-request]:opacity-70 [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-sm [&::-webkit-color-swatch]:border-0 [&::-moz-color-swatch]:rounded-sm [&::-moz-color-swatch]:border-0">

<!-- ─── Alpha + a <datalist> of preset swatches ─────────────────────── -->
<input type="color" name="accent" value="#22c55e" alpha list="brand-swatches" aria-label="Accent color" data-slot="color-picker"
  class="size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:rounded-sm [&::-webkit-color-swatch]:border-0 [&::-moz-color-swatch]:rounded-sm [&::-moz-color-swatch]:border-0">
<datalist id="brand-swatches">
  <option value="#e66465"></option>
  <option value="#1d4ed8"></option>
  <option value="#22c55e"></option>
</datalist>

<!--
  Boot script — mirrors each color-picker's hex <output> from its input value.
  Self-contained; safe to include once per page. (The docs site ships the same
  logic in public/site.js, so omit this there.)
-->
<script>
  (function () {
    document.addEventListener('input', function (e) {
      var input = e.target
      if (!input.matches || !input.matches('[data-slot="color-picker"] input[type="color"], input[data-slot="color-picker-swatch"]')) return
      var root = input.closest('[data-slot="color-picker"]')
      var out = root && root.querySelector('[data-slot="color-picker-value"]')
      if (out) out.textContent = input.value
    })
  })()
</script>

Examples

Hex readout

The native swatch plus a live <output> that mirrors the selected hex. Clicking it opens the platform color picker.

The control is a real <input type="color">, so the browser renders its own picker and coerces any invalid entry to a valid CSS color (applying :invalid when it can't). Since this input type has no implicit ARIA role, a visible <label for> or ariaLabel is required for an accessible name. The hex <output> is decorative (aria-hidden); the input remains the labelled source of truth.

<ColorPicker name="brand" value="#e66465" ariaLabel="Brand color" />
{{ color_picker(name="brand", value="#e66465", aria_label="Brand color") }}
{{template "color-picker" (dict "Name" "brand" "Value" "#e66465" "AriaLabel" "Brand color")}}
<.color_picker name="brand" value="#e66465" aria-label="Brand color" />
<div class="grid w-full max-w-3xs gap-2">
  <label class="text-xs font-medium" for="ex-basic-brand">Brand color</label>
  <span data-slot="color-picker" class="inline-flex items-center gap-2">
    <input type="color" id="ex-basic-brand" name="brand" value="#e66465" data-slot="color-picker-swatch" class="size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&amp;.htmx-request]:opacity-70 [&amp;::-webkit-color-swatch-wrapper]:p-0 [&amp;::-webkit-color-swatch]:rounded-sm [&amp;::-webkit-color-swatch]:border-0 [&amp;::-moz-color-swatch]:rounded-sm [&amp;::-moz-color-swatch]:border-0"/>
    <output data-slot="color-picker-value" aria-hidden="true" class="font-mono text-sm tabular-nums text-muted-foreground uppercase select-none">#e66465</output>
  </span>
</div>

Bare swatch — zero JavaScript

showValue={false} drops the hex readout, leaving just the native swatch. No wrapper, no script.

When you don't need the text readout, the component renders the plain native input. Pass alpha to let users edit transparency, or list pointing at a <datalist> to offer preset swatches — both are native features of the color input, no JS required.

<ColorPicker name="bg" value="#1d4ed8" alpha showValue={false} />
{{ color_picker(name="bg", value="#1d4ed8", alpha=true, show_value=false) }}
{{template "color-picker" (dict
  "Name" "bg" "Value" "#1d4ed8" "Alpha" true "NoValue" true)}}
<.color_picker name="bg" value="#1d4ed8" alpha show_value={false} />
<div class="grid w-full max-w-xs gap-2">
  <label class="text-xs font-medium" for="ex-bare-bg">Background color</label>
  <input type="color" id="ex-bare-bg" name="bg" value="#1d4ed8" alpha="" data-slot="color-picker" class="size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&amp;.htmx-request]:opacity-70 [&amp;::-webkit-color-swatch-wrapper]:p-0 [&amp;::-webkit-color-swatch]:rounded-sm [&amp;::-webkit-color-swatch]:border-0 [&amp;::-moz-color-swatch]:rounded-sm [&amp;::-moz-color-swatch]:border-0"/>
</div>

htmx — live preview

On the change event (fired when the picker is dismissed), htmx POSTs the chosen color and the server swaps in a styled preview chip.

htmx listens on the input's native change event, which fires when the platform picker is dismissed (use hx-trigger="input" to react to every adjustment instead). hx-target points at the preview node and the server returns the new chip — no client state.

#7c3aed

<ColorPicker name="color" value="#7c3aed"
            hx-post="/api/preview" hx-target="#preview"
            hx-swap="innerHTML" hx-trigger="change" />
<span id="preview" aria-live="polite"></span>
{{ color_picker(name="color", value="#7c3aed",
                hx_post="/api/preview", hx_target="#preview",
                hx_swap="innerHTML", hx_trigger="change") }}
<span id="preview" aria-live="polite"></span>
{{template "color-picker" (dict
  "Name" "color" "Value" "#7c3aed"
  "Attrs" (dict
    "hx-post" "/api/preview" "hx-target" "#preview"
    "hx-swap" "innerHTML" "hx-trigger" "change"
  ))}}
<.color_picker name="color" value="#7c3aed"
              hx-post="/api/preview" hx-target="#preview"
              hx-swap="innerHTML" hx-trigger="change" />
<div class="grid w-full max-w-3xs gap-3">
  <label class="text-xs font-medium" for="ex-htmx-color">Theme color</label>
  <span data-slot="color-picker" class="inline-flex items-center gap-2" hx-post="/color-picker/preview" hx-target="#ex-htmx-preview" hx-swap="innerHTML" hx-trigger="change">
    <input type="color" id="ex-htmx-color" name="color" value="#7c3aed" data-slot="color-picker-swatch" class="size-9 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1 shadow-xs outline-none transition-[color,box-shadow] dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 [&amp;.htmx-request]:opacity-70 [&amp;::-webkit-color-swatch-wrapper]:p-0 [&amp;::-webkit-color-swatch]:rounded-sm [&amp;::-webkit-color-swatch]:border-0 [&amp;::-moz-color-swatch]:rounded-sm [&amp;::-moz-color-swatch]:border-0"/>
    <output data-slot="color-picker-value" aria-hidden="true" class="font-mono text-sm tabular-nums text-muted-foreground uppercase select-none">#7c3aed</output>
  </span>
  <p class="text-sm text-muted-foreground" aria-live="polite">
    <span id="ex-htmx-preview">
      <span class="inline-flex items-center gap-2">
        <span class="inline-block size-4 rounded-sm border" style="background:#7c3aed">
        </span>
        <span class="font-mono text-foreground">#7c3aed</span>
      </span>
    </span>
  </p>
</div>

API Reference

<ColorPicker>

PropTypeDefaultDescription
valuestring"#000000"
A CSS <color> value. The browser coerces invalid input to a valid color and defaults to #000000 when omitted or unparseable.MDNcolor input value
showValuebooleantrue
Render a live hex <output> next to the swatch. false renders a bare native swatch (zero JS).
alphabooleanfalse
Allow editing the color's alpha channel. Experimental — ignored where the engine does not support it.MDNalpha attribute
colorspace"limited-srgb"|"display-p3""limited-srgb"
Hints the picker's color space and gamut. Experimental.MDNcolorspace attribute
liststring
Id of a <datalist> of preset color swatches the browser offers in the picker.MDNlist / <datalist>
autocompletestring
Browser auto-fill hint. One of the few common attributes the color input supports.
idstring
Pairs the input with a <label for>.
namestring
Form field name on submit.
requiredbooleanfalse
Native HTML required for form validation.
disabledbooleanfalse
Disable — unfocusable, not submitted.
autofocusbooleanfalse
Focus this swatch on initial page load (one per document).
formstring
Associate with a <form> by id when rendered outside it.
ariaInvalidboolean|"grammar"|"spelling"
Mark the swatch invalid — drives the destructive border/ring styling.MDNaria-invalid
ariaRequiredboolean
Expose required state to assistive tech.
ariaLabelstring
Accessible name when no visible <label>.MDNaria-label
ariaLabelledbystring
Id of a visible element providing the accessible name.MDNaria-labelledby
ariaDescribedbystring
Id of an element describing this control (announced after the name).MDNaria-describedby
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference