beta

Color Picker <mv-color-picker>

Color well that opens a light-dismiss popover (or sits inline): a saturation/brightness area, hue and opacity sliders, one text field that accepts any CSS color with a HEX/RGB/HSL/OKLCH switch, the eyedropper, swatches and recent colors. Every slider announces a readable color name, and the element is a form field whose value is a CSS color.

CategoryForms
TypeWeb Component (<mv-color-picker>)
Statusbeta
Keywordscolor, picker, oklch, hex, hsl, eyedropper, swatches, popover, form-associated, editor

When to use

  • Users choose an arbitrary color in a theme builder, design tool or brand settings page
  • A form field should submit a CSS color, with swatches and recent colors for quick reuse
  • Colors are pasted or typed in any CSS syntax and edited in HEX, RGB, HSL or OKLCH with opacity

Avoid when

  • Shoppers choose a product color among a few fixed variants → use Swatch instead
  • The value is a multi-stop gradient rather than a single color → use Gradient Editor instead

Install

node scripts/add.mjs color-picker --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["color-picker"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/canvas.js, core/dom.js, core/element.js, core/focus.js, core/form.js, core/position.js, components/color-picker/color-picker.js, components/color-picker/color-picker.css.

Usage

Canonical markup, to start from and customize with attributes, data-* and CSS variables:

<div id="cp-demo">
  <style>
    #cp-demo { display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: flex-start; justify-content: center; width: 100%; }
    #cp-demo .cpd-col { display: grid; gap: 1rem; width: 17rem; }
    #cp-demo .cpd-preview { display: grid; place-items: center; height: 9.5rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-xl); background: var(--cpd-surface, var(--mv-bg)); transition: background var(--mv-duration-fast); }
    #cp-demo .cpd-ticket { display: grid; gap: .2rem; min-width: 11rem; padding: .9rem 1rem; border-radius: .9rem; background: var(--cpd-brand); color: var(--cpd-ink); font: 600 .95rem/1.25 var(--mv-font-sans); box-shadow: 0 16px 32px -18px var(--cpd-brand); }
    #cp-demo .cpd-ticket small { font: 500 .65rem/1.4 var(--mv-font-mono); opacity: .8; }
    #cp-demo .cpd-form { display: grid; gap: .6rem; padding: .9rem; border: 1px solid var(--mv-border); border-radius: var(--mv-radius-lg); background: var(--mv-surface); }
    #cp-demo .cpd-head { margin: 0; font: 600 .7rem var(--mv-font-sans); letter-spacing: .06em; text-transform: uppercase; color: var(--mv-fg-muted); }
    #cp-demo .cpd-row { display: grid; grid-template-columns: 4.75rem 1fr; align-items: center; gap: .5rem; font-size: .8rem; color: var(--mv-fg-muted); }
    #cp-demo .cpd-row mv-color-picker { width: 100%; }
    #cp-demo .cpd-marks { display: flex; align-items: center; gap: .5rem; font-size: .8rem; color: var(--mv-fg-muted); }
  </style>
  <div class="cpd-col">
    <div class="cpd-preview" id="cp-demo-preview">
      <div class="cpd-ticket">Summer Pass · $49<small id="cp-demo-out"></small></div>
    </div>
    <form class="cpd-form" aria-label="Theme" onsubmit="return false">
      <p class="cpd-head">Theme</p>
      <div class="cpd-row"><label for="cp-demo-brand">Brand</label>
        <mv-color-picker id="cp-demo-brand" name="brand" value="#7C3AED"></mv-color-picker>
      </div>
      <div class="cpd-row"><label for="cp-demo-surface">Surface</label>
        <mv-color-picker id="cp-demo-surface" name="surface" format="oklch" value="oklch(94% 0.03 300 / 0.8)"></mv-color-picker>
      </div>
      <div class="cpd-row"><label for="cp-demo-ink">Text</label>
        <mv-color-picker id="cp-demo-ink" name="ink" value="rgb(255 255 255)" opaque swatches="#FFFFFF, #F8FAFC, #FDE68A, #1E1B4B, #0F172A"></mv-color-picker>
      </div>
      <div class="cpd-marks">Tags
        <mv-color-picker data-variant="swatch" label="Tag Sale" value="#F97316" opaque></mv-color-picker>
        <mv-color-picker data-variant="swatch" label="Tag New" value="#14B8A6" opaque></mv-color-picker>
        <mv-color-picker data-variant="swatch" label="Tag Sold out" value="#64748B" opaque></mv-color-picker>
      </div>
    </form>
  </div>
  <mv-color-picker inline label="Accent" value="#EC4899" no-recent></mv-color-picker>
</div>
<script type="module">
  const $ = (id) => document.getElementById(`cp-demo-${id}`);
  const apply = () => {
    const p = $("preview");
    p.style.setProperty("--cpd-brand", $("brand").value);
    p.style.setProperty("--cpd-surface", $("surface").value);
    p.style.setProperty("--cpd-ink", $("ink").value);
    $("out").textContent = $("brand").value;
  };
  document.getElementById("cp-demo").addEventListener("mv-input", apply);
  customElements.whenDefined("mv-color-picker").then(() => requestAnimationFrame(apply));
</script>

API

Attributes

NameTypeDefaultDescription
valuestring#000000Initial CSS color (hex, rgb(), hsl(), hwb(), oklch(), oklab(), name, var()…). Restored on form reset.
formathex | rgb | hsl | oklchinferred from valueOutput and field format; the user can switch it in the panel.
labelstringColorAccessible name of the well and the panel (falls back to an associated <label>).
swatchesstringComma-separated colors for the swatch row; an empty string hides it.
inlinebooleanPanel rendered in place, with no well or popover; mv-change fires at the end of each gesture.
opaquebooleanHides opacity (alpha is always 1).
no-recentbooleanNeither reads nor saves recent colors (shared localStorage).
placementstringbottom-startPopover placement, flipped and shifted to stay on screen.
name / disabled / readonlyStandard form behavior; readonly still opens the panel to read and copy the value.
data-variantswatchRound swatch only, no text.

Properties

NameTypeDescription
valuestringCSS color in the current format (an oklch value is kept exactly, even outside the sRGB gamut). Setting it does not fire events.
rgba{ r, g, b, a }Channels from 0 to 1.
hexstring“#RRGGBB”, or “#RRGGBBAA” when translucent.
modestringCurrent format (read and write).
isOpenbooleanWhether the popover is open.
stringsobjectOverrides for every visible and announced label, color names included (i18n).

Methods

NameDescription
show() / hide()Opens or closes the popover; closing with a new color fires mv-change.
parseColor(str, el?) / formatColor(rgba, mode, lch?) / mixColors(a, b, t) / colorName(rgba)Module exports: parsing (null when not a color), formatting, sRGB mix, readable name. HSV, HSL and OKLCH conversions are exported too.

Events

NameDescription
mv-inputLive change (drag, keys, typing, swatch). detail: { value, hex, rgba }.
mv-changePopover closed with a new color, or end of a gesture when inline. The color joins the recent ones.
mv-open / mv-closePopover opened and closed.

CSS classes

NameDescription
mv-color-picker-trigger / -chip / -panel / -area / -hue / -alpha / -compare / -fields / -format / -input / -swatchGenerated parts.

CSS variables

NameDefaultDescription
--mv-color-picker-width8.5remWell width.
--mv-color-picker-panel-width17.5remPanel width.
--mv-color-picker-area-height9.5remHeight of the saturation/brightness area.
--mv-color-picker-columns10Swatches per row.

Accessibility

The well is a button (aria-haspopup=dialog, aria-expanded) whose name holds the label, a readable color name and the value. The popover is a named dialog that closes on outside click, Escape, Enter or when focus leaves it, and focus returns to the well. The 2D area is a slider (aria-valuetext “Saturation 80%, brightness 60%, dark violet”) moved with the arrow keys (Shift ×10, Page Up/Down for brightness, Home/End for saturation). Hue and opacity are sliders that also name the color. The format is a native select, the text field accepts any CSS color and its arrow keys nudge the number under the caret; invalid text sets aria-invalid. Swatch groups have one tab stop each and arrow-key navigation, with aria-pressed on the current color. Forced colors keep the color surfaces and switch outlines, thumbs and focus to system colors.