Custom fields
This walkthrough adds a ColorPicker field — a native <input type="color"> — to a Lattice form.
1. Publish the JS scaffold
Section titled “1. Publish the JS scaffold”If you have not done this yet, publish the registration file the generators expect:
php artisan vendor:publish --tag=lattice-jsThis writes a single resources/js/registry.ts if it does not already exist — the one place custom fields, components, and columns are registered.
2. Generate the field
Section titled “2. Generate the field”php artisan lattice:field ColorPickerThis creates:
app/Forms/Fields/ColorPicker.php— the PHP class.resources/js/fields/color-picker.tsx— the React renderer stub.- An entry under
componentsinresources/js/registry.tswiring them together. - Runs
lattice:typescriptto refresh the generated types file.
The PHP attribute receives the short identifier color-picker; the wire type is field.color-picker.
Pass --type= to override it.
3. The generated PHP class
Section titled “3. The generated PHP class”<?php
namespace App\Forms\Fields;
use Lattice\Form\Attributes\AsField;use Lattice\Form\Components\Field;
#[AsField(type: 'color-picker')]class ColorPicker extends Field{ //}Field provides the standard field API — make(), required(), disabled(), value(), and the rest. Add public properties for any extra data you want available in the renderer:
#[AsField(type: 'color-picker')]class ColorPicker extends Field{ public ?string $swatches = null;
public function swatches(string $swatches): static { $this->swatches = $swatches;
return $this; }}4. The generated React renderer
Section titled “4. The generated React renderer”import type { RendererComponent } from "@lattice-php/lattice";
export const ColorPickerComponent: RendererComponent<"field.color-picker"> = ({ node }) => { // Render the field.color-picker field. Field state is available on node.props. return <div data-lattice-field={String(node.props.name ?? "")} />;};Replace the stub body with real UI:
import type { RendererComponent } from "@lattice-php/lattice";
export const ColorPickerComponent: RendererComponent<"field.color-picker"> = ({ node }) => { return ( <input type="color" name={String(node.props.name ?? "")} defaultValue={String(node.props.value ?? "#000000")} /> );};node.props contains all the serialized field data — name, value, label, required, and any extra properties you added to the PHP class.
To give a custom control the same label, required marker, helper text, tooltip, and error frame the built-in fields use, wrap it in FormField from @lattice-php/ui. It passes the id and aria-* wiring for the control through a render prop; pass bare to keep only a visually hidden label, as fields inside a repeater table cell do:
import { FormField } from "@lattice-php/ui";
<FormField id="color" label="Color" helperText="Pick a swatch" error={error} required> {(controlProps) => <input {...controlProps} type="color" />}</FormField>;5. The registry entry
Section titled “5. The registry entry”The generator appended an entry to resources/js/registry.ts, wrapping the renderer in eagerComponent:
import { eagerComponent, extendRegistry, registry as packageRegistry } from "@lattice-php/lattice";import type { Plugin } from "@lattice-php/lattice";import { ColorPickerComponent } from "./fields/color-picker";
export const registry = extendRegistry(packageRegistry, { name: "app", components: { "field.color-picker": eagerComponent(ColorPickerComponent), },} satisfies Plugin);The plugin object accepts any number of component type keys. Use lazyComponent instead for a code-split renderer — its loader must resolve to a module with a default export:
"field.color-picker": lazyComponent(async () => ({ default: (await import("./fields/color-picker")).ColorPickerComponent,})),6. Wire the registry in app.tsx
Section titled “6. Wire the registry in app.tsx”registry.ts already merges your plugin onto the built-in registry. Pass its exported registry to createLatticeApp — the same one-call bootstrap from installation, now made aware of your custom components:
import "../css/app.css";import { createLatticeApp } from "@lattice-php/lattice";import plugins from "virtual:lattice/plugins";import sprite from "virtual:svg-sprite";import { registry } from "./registry";
createLatticeApp({ registry, plugins, sprite, pages: import.meta.glob("./Pages/**/*.tsx"),});Passing registry is what makes your field render. Without it, createLatticeApp falls back to the built-in registry, your custom type has no renderer, and the node renders a muted missing-component placeholder instead of your field (Lattice also logs a [lattice] No component registered… warning in development to flag exactly this).
7. Generate TypeScript types
Section titled “7. Generate TypeScript types”php artisan lattice:typescriptThis writes resources/js/lattice/generated.d.ts, which augments the ComponentProps interface in @lattice-php/core:
declare module "@lattice-php/core" { interface ComponentProps { "field.color-picker": { swatches: string | null; }; }}After running this command, node.props.swatches is typed in your renderer.
8. Use the field in a form
Section titled “8. Use the field in a form”use App\Forms\Fields\ColorPicker;use Illuminate\Http\Request;use Lattice\Form\Attributes\AsForm;use Lattice\Form\Components\Form as FormComponent;use Lattice\Form\FormData;use Lattice\Form\FormDefinition;use Symfony\Component\HttpFoundation\Response;
#[AsForm('app.brand-settings')]final class BrandSettingsForm extends FormDefinition{ public function definition(FormComponent $form, Request $request): FormComponent { return $form->schema([ ColorPicker::make('brand_color', 'Brand color') ->swatches('#ff0000,#00ff00,#0000ff') ->value('#6366f1'), ]); }
public function handle(FormData $data): Response { // persist $data->string('brand_color') …
return redirect()->back(); }}The field serializes to a node with type: "field.color-picker" and the renderer picks it up automatically.