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.json2. Use it
import { ColorPicker } from "@/components/ui/color-picker"
<ColorPicker name="brand" value="#e66465" ariaLabel="Brand color" />Or copy the source manually
/** @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
{% from "components/color-picker.html" import color_picker %}
{{ color_picker(name="brand", value="#e66465", aria_label="Brand color") }}View source
{# 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
tpl.ExecuteTemplate(w, "color-picker", map[string]any{
"Name": "brand", "Value": "#e66465", "AriaLabel": "Brand color",
})View source
{{/*
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
<.color_picker name="brand" value="#e66465" aria-label="Brand color" />View source
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
<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
<!--
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 [&.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>
</div>Further reading
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 [&.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"/>
</div>Further reading
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 [&.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">#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>Further reading
API Reference
<ColorPicker>
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | "#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 |
showValue | boolean | true | Render a live hex <output> next to the swatch. false renders a bare native swatch (zero JS). |
alpha | boolean | false | 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 |
list | string | — | Id of a <datalist> of preset color swatches the browser offers in the picker.MDNlist / <datalist> |
autocomplete | string | — | Browser auto-fill hint. One of the few common attributes the color input supports. |
id | string | — | Pairs the input with a <label for>. |
name | string | — | Form field name on submit. |
required | boolean | false | Native HTML required for form validation. |
disabled | boolean | false | Disable — unfocusable, not submitted. |
autofocus | boolean | false | Focus this swatch on initial page load (one per document). |
form | string | — | Associate with a <form> by id when rendered outside it. |
ariaInvalid | boolean|"grammar"|"spelling" | — | Mark the swatch invalid — drives the destructive border/ring styling.MDNaria-invalid |
ariaRequired | boolean | — | Expose required state to assistive tech. |
ariaLabel | string | — | Accessible name when no visible <label>.MDNaria-label |
ariaLabelledby | string | — | Id of a visible element providing the accessible name.MDNaria-labelledby |
ariaDescribedby | string | — | Id of an element describing this control (announced after the name).MDNaria-describedby |
class | string | — | Extra Tailwind classes appended to the root element. |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |