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 casessecondary- 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 whenisRequiredis true - Disabled:
[data-disabled="true"]- Applied whenisDisabledis 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
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | (values: ColorFieldRenderProps) => React.ReactNode | - | Child components (Label, ColorField.Group, etc.) or render function. |
className | string | (values: ColorFieldRenderProps) => string | - | CSS classes for styling, supports render props. |
style | React.CSSProperties | (values: ColorFieldRenderProps) => React.CSSProperties | - | Inline styles, supports render props. |
fullWidth | boolean | false | Whether the color field should take full width of its container |
id | string | - | The element's unique identifier. |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, ColorFieldRenderProps> | - | Overrides the default DOM element with a custom render function. |
Value Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | Color | null | - | Current value (controlled). |
defaultValue | Color | null | - | Default value (uncontrolled). |
onChange | (color: Color | null) => void | - | Handler called when the value changes. |
Channel Props
| Prop | Type | Default | Description |
|---|---|---|---|
colorSpace | ColorSpace | - | The color space that the color field operates in when channel is provided. |
channel | ColorChannel | - | The color channel to edit. If not provided, edits hex value. |
Validation Props
| Prop | Type | Default | Description |
|---|---|---|---|
isRequired | boolean | false | Whether user input is required before form submission. |
isInvalid | boolean | - | 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
| Prop | Type | Default | Description |
|---|---|---|---|
isDisabled | boolean | - | Whether the input is disabled. |
isReadOnly | boolean | - | Whether the input can be selected but not changed. |
isWheelDisabled | boolean | - | Whether to disable changing the value with scroll. |
Form Props
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | - | Name of the input element, for HTML form submission. |
autoFocus | boolean | - | Whether the element should receive focus on render. |
Accessibility Props
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | - | Accessibility label when no visible label is present. |
aria-labelledby | string | - | ID of elements that label this field. |
aria-describedby | string | - | ID of elements that describe this field. |
aria-details | string | - | 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:
| Prop | Type | Description |
|---|---|---|
isDisabled | boolean | Whether the field is disabled. |
isInvalid | boolean | Whether the field is currently invalid. |
isReadOnly | boolean | Whether the field is read-only. |
isRequired | boolean | Whether the field is required. |
isFocused | boolean | Whether the field is currently focused. |
isFocusWithin | boolean | Whether any child element is focused. |
isFocusVisible | boolean | Whether focus is visible (keyboard navigation). |
ColorField.Group
ColorField.Group accepts all props from React Aria's Group component plus the following:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Tailwind classes merged with the component styles. |
fullWidth | boolean | false | Whether 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. |
render | DOMRenderFunction<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:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Tailwind classes merged with the component styles. |
placeholder | string | - | Placeholder text shown when empty. |
ColorField.Prefix
ColorField.Prefix accepts standard HTML div attributes:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Tailwind classes merged with the component styles. |
children | ReactNode | - | Content to display in the prefix slot. |
ColorField.Suffix
ColorField.Suffix accepts standard HTML div attributes:
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Tailwind classes merged with the component styles. |
children | ReactNode | - | 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:
:hoveror[data-hovered="true"] - Focus Within:
[data-focus-within="true"]or:focus-within - Invalid:
[data-invalid="true"](also syncs witharia-invalid) - Disabled:
[data-disabled="true"]or[aria-disabled="true"]