Chroma Panel

Search documentation

Find a page or section

React Hook Form

View as Markdown

Two ways to wire it up: a name, or Controller.

chroma-panel ships no React Hook Form integration, and it does not need one. You can use the React color picker with React Hook Form in two ways: leave the field uncontrolled and let ColorInput submit itself, or wrap it in <Controller> so the form owns the value. This page shows both, and which of the two change events to validate on.

Install

npm install chroma-panel

React Hook Form is not part of chroma-panel. The examples below assume you already have it in your app and use the v7 Controller API.

Uncontrolled: let ColorInput submit itself

Give ColorInput a name and it renders a hidden input that submits with the surrounding form, in whichever format you set. React Hook Form never sees the field, so you read it from the form data yourself:

import { useRef } from "react";
import { useForm } from "react-hook-form";
import { ColorInput } from "chroma-panel";
 
type Values = { label: string };
 
export function BrandForm() {
  const formRef = useRef<HTMLFormElement>(null);
  const { register, handleSubmit } = useForm<Values>({
    defaultValues: { label: "" },
  });
 
  const onSubmit = (values: Values): void => {
    const form = formRef.current;
 
    if (form === null) {
      return;
    }
 
    const brand = String(new FormData(form).get("brand") ?? "");
 
    save({ label: values.label, brand });
  };
 
  return (
    <form ref={formRef} onSubmit={handleSubmit(onSubmit)}>
      <input {...register("label")} />
      <ColorInput
        name="brand"
        defaultValue="#3366cc"
        aria-label="Brand color"
      />
      <button type="submit">Save</button>
    </form>
  );
}

This is the smaller of the two. The cost is that the color stays outside the form state: it does not show up in formState, watch or a resolver, and reset() does not touch it. form.reset() still returns the control to its defaultValue, because that is native behavior. Pick this when the color needs no validation beyond required, which ColorInput handles natively — see forms.

Controller: let the form own the value

Wrap ColorInput in <Controller> and it becomes an ordinary controlled field. Pass value and onChange yourself rather than spreading field:

import { Controller, useForm } from "react-hook-form";
import { ColorInput, type ColorChangeResult } from "chroma-panel";
 
type Values = { brand: string };
 
export function BrandForm() {
  const { control, handleSubmit } = useForm<Values>({
    defaultValues: { brand: "#3366cc" },
  });
 
  return (
    <form onSubmit={handleSubmit((values) => save(values))}>
      <Controller
        name="brand"
        control={control}
        rules={{ required: "Pick a color" }}
        render={({ field, fieldState }) => (
          <div>
            <ColorInput
              value={field.value}
              onChange={(result: ColorChangeResult) =>
                field.onChange(result.hex)
              }
              onChangeComplete={() => field.onBlur()}
              aria-label="Brand color"
            />
            {fieldState.error && <p role="alert">{fieldState.error.message}</p>}
          </div>
        )}
      />
      <button type="submit">Save</button>
    </form>
  );
}

Two things to keep straight:

  • Do not spread {...field} onto ColorInput. field.onChange would be handed a color object instead of the string you want stored, and field.ref would not attach, because ColorInput takes no ref.
  • Leave name off ColorInput here. With a name it renders its own hidden input, which would submit the color a second time alongside the value the form already holds.

field.value is a string, which is what ColorInput expects. It also accepts an Hsva object if you would rather store the color without the rounding that hex applies — the trade-off is in controlled and uncontrolled.

Which event to validate on

onChange fires continuously while you drag the panel. onChangeComplete fires once, when you let go. That distinction decides how your validation mode behaves:

useForm modeWhat happens
"onSubmit" (default)Validates once on submit. Nothing to tune.
"onBlur"Validates when you call field.onBlur().
"onChange"Revalidates on every frame of a drag.

The example above calls field.onBlur() from onChangeComplete, so with mode: "onBlur" the field is checked once per drag rather than sixty times a second. If you are on mode: "onChange" and the form feels heavy, that is the first thing to change.

onChange and onChangeComplete are both listed with the rest of the props on ColorInput.