ComfyCoders TranquilTools
Docs /Laravel Vue FormBuilder
Repository
v1

Theming

Every field renders the same chrome around its own control: a wrapper, a label, help text and an error message. Those four are themeable from PHP, and any field can be replaced outright with a slot when classes are not enough.

The four elements

Key Element Default
wrapper the div around the whole field space-y-1
label the <label> block text-sm font-medium text-neutral-800 dark:text-neutral-200
help the help text under the control mt-1 text-xs text-neutral-500 dark:text-neutral-400
error the validation message text-sm text-red-600

Application-wide

Publish the config and set the keys you want to change:

// config/form-builder.php
'theme' => [
    'label' => 'text-sm uppercase tracking-wide text-slate-600',
    'error' => 'text-sm text-rose-700',
],

A key left null or empty keeps the renderer's default rather than blanking it. The theme travels in the schema, so it is the PHP core that owns it — any renderer built on this contract inherits the same classes.

Per field

Text::make('title')->theme(['label' => 'sr-only']);

Repeated calls merge, and keys you leave out fall back to the application-wide theme.

How the layers combine

Defaults, then the config theme, then the field theme — each merged over the previous one through tailwind-merge. A conflicting utility wins over the one it conflicts with instead of both landing in the class list:

'theme' => ['error' => 'text-rose-700']
// renders class="text-sm text-rose-700" — the size survives, the colour is replaced

This makes the theme a tool for adjusting the defaults. To discard them entirely, use a slot.

->class('...') still replaces the wrapper outright rather than merging into it, which is what it has always done. Use ->theme(['wrapper' => '...']) to add to the wrapper instead.

Replacing a field entirely

<Form> accepts a slot per field name, and per field type as a broad stroke. A slot named after a field wins over one named after its type.

<Form :schema="form">
    <template #title="{ field, modelValue, error, 'onUpdate:modelValue': update }">
        <MyOwnInput :label="field.label" :model-value="modelValue" @update:model-value="update" />
        <span v-if="error">{{ error }}</span>
    </template>

    <template #wysiwyg="{ field }">
        <MyEditor :name="field.name" />
    </template>
</Form>

Slot props are field, form, error, modelValue and onUpdate:modelValue. Writing through onUpdate:modelValue keeps the Inertia form object in sync and fires the form's onFieldChange, exactly as a built-in field does.

Slots reach fields nested inside a Repeater as well, so a #title slot applies to a title sub-field in every row.