Skip to content
Lenso UI

ColorField

Color input field with labels, descriptions, and validation built on React Aria ColorField

Usage

import { ColorField, parseColor } from '@lenso/ui';
"use client";
import { ColorField, ColorSwatch, parseColor } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({ field: { width: 280, maxWidth: "100%" } });export function Basic() {  const [color, setColor] = useState<ReturnType<typeof parseColor> | null>(() =>    parseColor("#0485F7"),  );  return (    <ColorField xstyle={styles.field} name="color" value={color} onChange={setColor}>      <ColorField.Label>Color</ColorField.Label>      <ColorField.Group>        <ColorField.Prefix>          <ColorSwatch {...(color ? { color } : {})} size="xs" />        </ColorField.Prefix>        <ColorField.Input />      </ColorField.Group>    </ColorField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Anatomy

import {ColorField, Label, ColorSwatch, Description, FieldError, parseColor} from '@lenso/ui';
export default () => (  <ColorField>    <Label />    <ColorField.Group>      <ColorField.Prefix>        <ColorSwatch color="#000000" />      </ColorField.Prefix>      <ColorField.Input />    </ColorField.Group>    <Description />    <FieldError />  </ColorField>)

ColorField combines label, color input, description, and error into a single accessible component.

Examples

Variants

The ColorField.Group component supports two visual variants:

  • primary (default) - Standard styling with shadow, suitable for most use cases
  • secondary - Lower emphasis variant without shadow, suitable for use in Surface components
"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { ColorField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "../color-picker/source.stylex";export function Variants() {  return (    <div {...stylex.props(styles.column)}>      <ColorField xstyle={styles.width280} defaultValue="#0485F7" name="primary-color">        <ColorField.Label>Primary variant</ColorField.Label>        <ColorField.Group variant="primary">          <ColorField.Input />        </ColorField.Group>      </ColorField>      <ColorField xstyle={styles.width280} defaultValue="#F43F5E" name="secondary-color">        <ColorField.Label>Secondary variant</ColorField.Label>        <ColorField.Group variant="secondary">          <ColorField.Input />        </ColorField.Group>      </ColorField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

On Surface

When used inside a Surface component, use variant="secondary" on ColorField.Group to apply the lower emphasis variant suitable for surface backgrounds.

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { ColorField, Surface } from "@lenso/ui";import { styles } from "../color-picker/source.stylex";export function OnSurface() {  return (    <Surface xstyle={[styles.width320, styles.padded]}>      <ColorField defaultValue="#3B82F6" name="color">        <ColorField.Label>Theme Color</ColorField.Label>        <ColorField.Group variant="secondary">          <ColorField.Input />        </ColorField.Group>        <ColorField.Description>Select your theme color</ColorField.Description>      </ColorField>    </Surface>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

With Description

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { ColorField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "../color-picker/source.stylex";export function WithDescription() {  return (    <div {...stylex.props(styles.column)}>      <ColorField xstyle={styles.width280} defaultValue="#3B82F6" name="color">        <ColorField.Label>Primary Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input />        </ColorField.Group>        <ColorField.Description>Enter your brand's primary color</ColorField.Description>      </ColorField>      <ColorField xstyle={styles.width280} defaultValue="#F59E0B" name="accent-color">        <ColorField.Label>Accent Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input />        </ColorField.Group>        <ColorField.Description>Used for highlights and CTAs</ColorField.Description>      </ColorField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Required Field

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { ColorField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "../color-picker/source.stylex";export function Required() {  return (    <div {...stylex.props(styles.column)}>      <ColorField isRequired xstyle={styles.width280} name="color">        <ColorField.Label>Brand Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input placeholder="#000000" />        </ColorField.Group>      </ColorField>      <ColorField isRequired xstyle={styles.width280} name="theme-color">        <ColorField.Label>Theme Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input placeholder="#000000" />        </ColorField.Group>        <ColorField.Description>Required field</ColorField.Description>      </ColorField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Disabled State

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { ColorField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "../color-picker/source.stylex";export function Disabled() {  return (    <div {...stylex.props(styles.column)}>      <ColorField isDisabled xstyle={styles.width280} defaultValue="#0485F7" name="color">        <ColorField.Label>Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input />        </ColorField.Group>        <ColorField.Description>This color field is disabled</ColorField.Description>      </ColorField>      <ColorField isDisabled xstyle={styles.width280} name="color-empty">        <ColorField.Label>Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input placeholder="#000000" />        </ColorField.Group>        <ColorField.Description>This color field is disabled</ColorField.Description>      </ColorField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Full Width

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { ColorField } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";import { styles } from "../color-picker/source.stylex";export function FullWidth() {  return (    <div {...stylex.props(styles.width400, styles.column)}>      <ColorField fullWidth defaultValue="#10B981" name="color">        <ColorField.Label>Brand Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input />        </ColorField.Group>      </ColorField>      <ColorField fullWidth defaultValue="#8B5CF6" name="color-with-suffix">        <ColorField.Label>Theme Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input />        </ColorField.Group>      </ColorField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Validation

Use isInvalid together with FieldError to surface validation messages.

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { ColorField } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { styles } from "../color-picker/source.stylex";export function Invalid() {  // A Color cannot represent malformed text; keep the source's invalid edit in the input.  const [invalidText, setInvalidText] = useState("not-a-color");  return (    <div {...stylex.props(styles.column)}>      <ColorField isInvalid isRequired xstyle={styles.width280} name="color">        <ColorField.Label>Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input placeholder="#000000" />        </ColorField.Group>        <ColorField.Error>Please enter a valid hex color</ColorField.Error>      </ColorField>      <ColorField isInvalid xstyle={styles.width280} name="invalid-color">        <ColorField.Label>Background Color</ColorField.Label>        <ColorField.Group>          <ColorField.Input            value={invalidText}            onChange={(event) => setInvalidText(event.target.value)}          />        </ColorField.Group>        <ColorField.Error>Invalid color format. Use hex (e.g., #FF5733)</ColorField.Error>      </ColorField>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Channel Editing

ColorField supports editing individual color channels (hue, saturation, lightness, red, green, blue, alpha) by setting the colorSpace and channel props.

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { ColorField, ColorSwatch, parseColor } from "@lenso/ui";import type { Color } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { styles } from "../color-picker/source.stylex";export function ChannelEditing() {  const [color, setColor] = useState<Color | null>(parseColor("#7F007F"));  return (    <div {...stylex.props(styles.column)}>      <p {...stylex.props(styles.muted)}>Edit individual HSL channels:</p>      <div {...stylex.props(styles.controls)}>        {(["hue", "saturation", "lightness"] as const).map((channel) => (          <ColorField            key={channel}            channel={channel}            xstyle={styles.width100}            colorSpace="hsl"            name={channel}            value={color}            onChange={setColor}          >            <ColorField.Label xstyle={styles.capitalize}>{channel}</ColorField.Label>            <ColorField.Group>              <ColorField.Input />              {channel !== "hue" && (                <ColorField.Suffix>                  <span {...stylex.props(styles.muted)}>%</span>                </ColorField.Suffix>              )}            </ColorField.Group>          </ColorField>        ))}      </div>      <div {...stylex.props(styles.row2)}>        <ColorSwatch color={color ?? undefined} size="md" />        <span {...stylex.props(styles.small)}>          Current: {color ? color.toString("hex") : "(empty)"}        </span>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Controlled

Control the value to synchronize with other components or state management.

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { Button, ColorField, ColorSwatch, parseColor } from "@lenso/ui";import type { Color } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";import { styles } from "../color-picker/source.stylex";export function Controlled() {  const [value, setValue] = useState<Color | null>(parseColor("#0485F7"));  return (    <div {...stylex.props(styles.column)}>      <ColorField xstyle={styles.width280} name="color" value={value} onChange={setValue}>        <ColorField.Label>Color</ColorField.Label>        <ColorField.Group>          <ColorField.Prefix>            <ColorSwatch color={value ?? undefined} size="xs" />          </ColorField.Prefix>          <ColorField.Input />        </ColorField.Group>        <ColorField.Description>          Current value: {value ? value.toString("hex") : "(empty)"}        </ColorField.Description>      </ColorField>      <div {...stylex.props(styles.row2)}>        <Button variant="tertiary" onClick={() => setValue(parseColor("#EF4444"))}>          Set Red        </Button>        <Button variant="tertiary" onClick={() => setValue(parseColor("#10B981"))}>          Set Green        </Button>        <Button variant="tertiary" onClick={() => setValue(null)}>          Clear        </Button>      </div>    </div>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Form Example

Complete form example with validation and submission handling.

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. Native form and Base UI button retain the source submit workflow. */import { Button, ColorField, ColorSwatch } from "@lenso/ui";import type { Color } from "@lenso/ui";import { useState, type FormEvent } from "react";import * as stylex from "@stylexjs/stylex";import { styles } from "../color-picker/source.stylex";export function FormExample() {  const [value, setValue] = useState<Color | null>(null);  const [isSubmitting, setIsSubmitting] = useState(false);  function handleSubmit(event: FormEvent<HTMLFormElement>) {    event.preventDefault();    if (!value || isSubmitting) return;    setIsSubmitting(true);    setTimeout(() => {      setValue(null);      setIsSubmitting(false);    }, 1500);  }  return (    <form {...stylex.props(styles.column, styles.width280)} onSubmit={handleSubmit}>      <ColorField        fullWidth        isRequired        xstyle={styles.full}        name="brand-color"        value={value}        onChange={setValue}      >        <ColorField.Label>Brand Color</ColorField.Label>        <ColorField.Group>          <ColorField.Prefix>            <ColorSwatch color={value ?? undefined} size="xs" />          </ColorField.Prefix>          <ColorField.Input placeholder="#000000" />        </ColorField.Group>        <ColorField.Description>Choose your brand's primary color</ColorField.Description>      </ColorField>      <Button        xstyle={styles.full}        disabled={!value}        isLoading={isSubmitting}        type="submit"        variant="primary"      >        {isSubmitting ? "Saving..." : "Save Color"}      </Button>    </form>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Render Function

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. Native RAC render state replaces DOM render interception. */import { ColorField, ColorSwatch, parseColor } from "@lenso/ui";import type { Color } from "@lenso/ui";import { useState } from "react";import { styles } from "../color-picker/source.stylex";export function RenderFunction() {  const [color, setColor] = useState<Color | null>(parseColor("#0485F7"));  return (    <ColorField      xstyle={styles.width280}      name="color"      data-custom="foo"      value={color}      onChange={setColor}    >      {({ isInvalid }) => (        <>          <ColorField.Label>Color</ColorField.Label>          <ColorField.Group data-custom="foo" data-invalid={isInvalid || undefined}>            <ColorField.Prefix>              <ColorSwatch color={color ?? undefined} size="xs" />            </ColorField.Prefix>            <ColorField.Input />          </ColorField.Group>        </>      )}    </ColorField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Customization

Tailwind CSS

"use client";/** Adapted from HeroUI v3.2.6. Apache-2.0. */import { ColorField, ColorSwatch, parseColor } from "@lenso/ui";import type { Color } from "@lenso/ui";import { useState } from "react";import { styles } from "../color-picker/source.stylex";export function CustomStyles() {  const [color, setColor] = useState<Color | null>(parseColor("#6366F1"));  return (    <ColorField xstyle={styles.customField} name="accent-color" value={color} onChange={setColor}>      <ColorField.Label xstyle={styles.label}>Accent color</ColorField.Label>      <ColorField.Description>Applied to buttons, links, and focus rings.</ColorField.Description>      <ColorField.Group xstyle={styles.customGroup} variant="secondary">        <ColorField.Prefix>          <ColorSwatch xstyle={styles.square} color={color ?? undefined} size="xs" />        </ColorField.Prefix>        <ColorField.Input xstyle={styles.customInput} />      </ColorField.Group>    </ColorField>  );}

Local adaptation source above. Derived from HeroUI v3.2.6 source.

Global CSS

ColorField has minimal default styling. Override the .color-field class to customize the container styling.

@layer components {  .color-field {    @apply flex flex-col gap-1;
    &[data-invalid="true"],    &[aria-invalid="true"] {      [data-slot="description"] {        @apply hidden;      }    }
    [data-slot="label"] {      @apply w-fit;    }
    [data-slot="description"] {      @apply px-1;    }  }}

Styling Reference

HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

  • .color-field – Root container with minimal styling (flex flex-col gap-1)

Note: Child components (Label, Description, FieldError) have their own CSS classes and styling. See their respective documentation for customization options. ColorField.Group styling is documented below in the API Reference section.

Interactive States

ColorField automatically manages these data attributes based on its state:

  • Invalid: [data-invalid="true"] or [aria-invalid="true"] - Automatically hides the description slot when invalid
  • Required: [data-required="true"] - Applied when isRequired is true
  • Disabled: [data-disabled="true"] - Applied when isDisabled is true
  • Focus Within: [data-focus-within="true"] - Applied when any child input is focused

API Reference

ColorField

ColorField inherits all props from React Aria's ColorField component.

Base Props

PropTypeDefaultDescription
childrenReact.ReactNode | (values: ColorFieldRenderProps) => React.ReactNode-Child components (Label, ColorField.Group, etc.) or render function.
classNamestring | (values: ColorFieldRenderProps) => string-CSS classes for styling, supports render props.
styleReact.CSSProperties | (values: ColorFieldRenderProps) => React.CSSProperties-Inline styles, supports render props.
fullWidthbooleanfalseWhether the color field should take full width of its container
idstring-The element's unique identifier.
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, ColorFieldRenderProps>-Overrides the default DOM element with a custom render function.

Value Props

PropTypeDefaultDescription
valueColor | null-Current value (controlled).
defaultValueColor | null-Default value (uncontrolled).
onChange(color: Color | null) => void-Handler called when the value changes.

Channel Props

PropTypeDefaultDescription
colorSpaceColorSpace-The color space that the color field operates in when channel is provided.
channelColorChannel-The color channel to edit. If not provided, edits hex value.

Validation Props

PropTypeDefaultDescription
isRequiredbooleanfalseWhether user input is required before form submission.
isInvalidboolean-Whether the value is invalid.
validate(value: Color) => ValidationError | true | null | undefined-Custom validation function.
validationBehavior'native' | 'aria''native'Whether to use native HTML form validation or ARIA attributes.

State Props

PropTypeDefaultDescription
isDisabledboolean-Whether the input is disabled.
isReadOnlyboolean-Whether the input can be selected but not changed.
isWheelDisabledboolean-Whether to disable changing the value with scroll.

Form Props

PropTypeDefaultDescription
namestring-Name of the input element, for HTML form submission.
autoFocusboolean-Whether the element should receive focus on render.

Accessibility Props

PropTypeDefaultDescription
aria-labelstring-Accessibility label when no visible label is present.
aria-labelledbystring-ID of elements that label this field.
aria-describedbystring-ID of elements that describe this field.
aria-detailsstring-ID of elements with additional details.

Composition Components

ColorField works with these separate components that should be imported and used directly:

  • Label - Field label component from @lenso/ui
  • ColorField.Group - Color input group component (documented below)
  • ColorField.Input - Input element within ColorField.Group
  • ColorField.Prefix / ColorField.Suffix - Prefix and suffix slots for the input group
  • ColorSwatch - Color preview component from @lenso/ui
  • Description - Helper text component from @lenso/ui
  • FieldError - Validation error message from @lenso/ui

Each of these components has its own props API. Use them directly within ColorField for composition:

import {ColorField, Label, ColorSwatch, Description, FieldError, parseColor} from '@lenso/ui';
<ColorField  isRequired  isInvalid={hasError}  value={color}  onChange={setColor}>  <Label>Brand Color</Label>  <ColorField.Group>    <ColorField.Prefix>      <ColorSwatch color={color?.toString("hex") || "#E4E4E7"} />    </ColorField.Prefix>    <ColorField.Input />  </ColorField.Group>  <Description>Select your brand's primary color.</Description>  <FieldError>Please enter a valid color.</FieldError></ColorField>

Color Types

ColorField uses Color objects from React Aria Components:

import {parseColor} from '@lenso/ui';
// Parse from hex stringconst color = parseColor('#3B82F6');
// Get hex string from colorconst hex = color.toString('hex'); // "#3b82f6"
// Get RGB valuesconst rgb = color.toString('rgb'); // "rgb(59, 130, 246)"
// Use in ColorField<ColorField value={color} onChange={setColor}>  {/* ... */}</ColorField>

Render Props

When using render props with className, style, or children, these values are available:

PropTypeDescription
isDisabledbooleanWhether the field is disabled.
isInvalidbooleanWhether the field is currently invalid.
isReadOnlybooleanWhether the field is read-only.
isRequiredbooleanWhether the field is required.
isFocusedbooleanWhether the field is currently focused.
isFocusWithinbooleanWhether any child element is focused.
isFocusVisiblebooleanWhether focus is visible (keyboard navigation).

ColorField.Group

ColorField.Group accepts all props from React Aria's Group component plus the following:

PropTypeDefaultDescription
classNamestring-Tailwind classes merged with the component styles.
fullWidthbooleanfalseWhether the color input group should take full width of its container
variant"primary" | "secondary""primary"Visual variant of the component. primary is the default style with shadow. secondary is a lower emphasis variant without shadow, suitable for use in surfaces.
renderDOMRenderFunction<keyof React.JSX.IntrinsicElements, GroupRenderProps>-Overrides the default DOM element with a custom render function.

ColorField.Input

ColorField.Input accepts all props from React Aria's Input component plus the following:

PropTypeDefaultDescription
classNamestring-Tailwind classes merged with the component styles.
placeholderstring-Placeholder text shown when empty.

ColorField.Prefix

ColorField.Prefix accepts standard HTML div attributes:

PropTypeDefaultDescription
classNamestring-Tailwind classes merged with the component styles.
childrenReactNode-Content to display in the prefix slot.

ColorField.Suffix

ColorField.Suffix accepts standard HTML div attributes:

PropTypeDefaultDescription
classNamestring-Tailwind classes merged with the component styles.
childrenReactNode-Content to display in the suffix slot.

ColorField.Group Styling

Customizing the component classes

The base classes power every instance. Override them once with @layer components.

@layer components {  .color-input-group {    @apply inline-flex h-9 items-center overflow-hidden rounded-field border bg-field text-sm text-field-foreground shadow-field outline-none;
    &:hover,    &[data-hovered="true"] {      @apply bg-field-hover;    }
    &[data-focus-within="true"],    &:focus-within {      @apply status-focused-field;    }
    &[data-invalid="true"] {      @apply status-invalid-field;    }
    &[data-disabled="true"],    &[aria-disabled="true"] {      @apply status-disabled;    }  }
  .color-input-group__input {    @apply flex flex-1 items-center rounded-none border-0 bg-transparent px-3 py-2 shadow-none outline-none;  }
  .color-input-group__prefix,  .color-input-group__suffix {    @apply shrink-0 text-field-placeholder flex items-center;  }}

ColorField.Group CSS Classes

  • .color-input-group – Root container styling
  • .color-input-group__input – Input wrapper styling
  • .color-input-group__prefix – Prefix element styling
  • .color-input-group__suffix – Suffix element styling

ColorField.Group Interactive States

  • Hover: :hover or [data-hovered="true"]
  • Focus Within: [data-focus-within="true"] or :focus-within
  • Invalid: [data-invalid="true"] (also syncs with aria-invalid)
  • Disabled: [data-disabled="true"] or [aria-disabled="true"]