DXForm
The canonical form renderer. Drive it with field definitions; add tabs when
you need a multi-tab editor. Flat and tabbed forms share one engine
(DXField), so every field type, conditional
field, per-field slot override, async option, and nested
repeater works the same in both.
It works with the fetch-based useForm composable (no Inertia required) and
accepts either a useForm return or a defineForm return (with the latter,
fields may be omitted).
Live Examples
Product Editor
A tabbed form with conditional fields, a currency input, and auto-switching to the tab containing the first validation error.
Horizontal layout
Setting layout="horizontal" moves every field's label to a left-hand column, including checkbox and repeater fields. A field can opt back into vertical with field.layout.
Props
| Name Click to sort ascending | Type | Required Click to sort ascending | Default | Description |
|---|---|---|---|---|
form | UseFormReturn<any> | DefineFormReturn<any> | Yes | - | Form instance from useForm, or the { form, fields } object from defineForm. |
fields | FieldDefinition[] | No | - | Field definitions. Optional when form is a defineForm return (taken from it). |
tabs | FormTab[] | No | - | Tab definitions. When omitted, a flat single-column form renders. |
context | Record<string, any> | No | - | Extra context merged under the live form data when evaluating predicates (label/hint/when/disabled). |
activeTab | number (v-model) | No | 0 | Active tab index. Two-way bindable via v-model:active-tab. |
autoErrorTab | boolean | No | true | Auto-switch to the first tab containing a validation error. |
submitText | string | No | Submit | Submit button text. |
submitLoadingText | string | No | Submitting... | Submit button loading text. |
showSubmit | boolean | No | true | Show the submit button. |
card | boolean | No | false | Wrap a FLAT form in a card for a visual boundary (mirrors DXTable's card prop). Also forces the tabbed card on even when cardTabs is false. Off by default since DXForm is commonly embedded in a page card or modal already. |
cardTabs | boolean | No | true | Wrap a TABBED form's content in a card panel so the active tab reads as a finished panel connected to the tab strip (the standard Bootstrap card-with-tabs pattern) rather than floating on the bare page. Set false for bare tabs (e.g. inside a modal that already provides a boundary). Ignored for flat forms — use card for those. |
layout | "vertical" | "horizontal" | "auto" | No | "vertical" | Form-wide field layout. "horizontal" puts the label to the left of the input, always. "auto" picks horizontal or vertical from the width of the form's own container (see Container-driven layout below). Overridable per-field via field.layout. |
layoutThreshold | number | No | 640 | Container width (px) at or above which layout: "auto" goes horizontal. Ignored for the explicit layouts. |
labelCols | number | { sm?, md?, lg?, xl? } | No | 3 | Label column width for horizontal layout (mirrors BFormGroup's labelCols). Overridable per-field via field.labelCols. Omitted breakpoints collapse to vertical below that width. |
Events
| Name Click to sort ascending | Parameters | Description |
|---|---|---|
submit | none | Emitted when the form is submitted (run your form.post/put in the handler). |
Slots
Per-field slots are keyed by field key, e.g. #value(price) or #hint(sku).
#valuevs#span: a labelled custom control is a normal field plus a#value(<key>)slot — it keeps the form’s label column and grid, no re-derived col classes. Reserve#span(<key>)(withspan: true) for genuinely full-width, label-less blocks (a sub-table, an activity log).
| Name Click to sort ascending | Description | Scoped Props |
|---|---|---|
value(<key>) | Replace a field's input control with custom markup. | - |
span(<key>) | Full-width custom block for a field marked span: true. | - |
info(<key>) | Rich info block rendered always-visible below the field (not the label popover). | - |
info-popover(<key>) | Rich body for the label’s hover/focus info popover (lists, bold, paragraphs). Overrides the plain field.info text. | - |
hint(<key>) | Override the hint text rendered below the field. | - |
repeater-row(<key>) | Custom row layout for a repeater field. | - |
switch-list-item(<key>) | Extra content beside a switch-list row's toggle (e.g. a notes input bound to your own model). | - |
field(<key>) | Fully replace a field's rendering (including its label), bypassing DXField entirely. Supersedes field-before/field-after for the same key. | - |
field-before(<key>) | Content rendered directly above a field (e.g. a quick-create button above a select). | - |
field-after(<key>) | Content rendered directly below a field. | - |
tab-content(<key>) | Replace the entire body of a tab. | - |
tab-before(<key>) | Content rendered before a tab's fields. | - |
tab-after(<key>) | Content rendered after a tab's fields. | - |
footer | Content rendered after the form body. | - |
Container-driven layout
layout="horizontal" is unconditional, and that is a problem on a dashboard:
Bootstrap’s breakpoints are media queries, so they only ever see the
window. A page narrowed by the persistent sidebar — or a form inside a modal —
can have very little room while the viewport is wide, and no breakpoint fires.
The label/input split then cramps the labels and truncates the controls.
layout="auto" measures the form’s own container instead and stacks to
vertical below layoutThreshold:
<DXForm :form="form" :fields="fields" layout="auto" />
The default threshold is 640px. With the default 3-column label, the label’s
text area is roughly containerWidth / 4 - 18px, so 640 leaves it ~142px —
enough for a 20-character label such as “Unit price (ex VAT)” to stay on one
line, with the control column still ~474px. Raise layoutThreshold if your
labels run longer, or lower labelCols instead.
Notes:
vertical(the default) andhorizontalare unchanged and unconditional. No existing form changes appearance;autois opt-in.- Before the first measurement — and when server-rendering — the form renders vertical. A stacked form is legible at any width, so it is the safe thing to show when the width is not yet known.
- The crossing has a small hysteresis band, so a form that gains a scrollbar when it stacks cannot flip back and forth at the boundary.
- Per-field
field.layoutstill wins, and aspan: truefield is always full-width.
The measurement itself is available on its own — see useContainerWidth if you need to drive something else (a table that becomes cards, a column that collapses) off the same signal.
Field definitions
Each field is a FieldDefinition. Supported type values:
| Type | Renders |
|---|---|
text, email, password, number, url, tel, date, datetime-local, datetime, time | Native input |
textarea | Multi-line input |
select | Dropdown (sync options or async optionsLoader). Add searchable: true for a type-to-filter select that still models the option’s value |
autocomplete | Free-text input with a <datalist> of suggestions (sync options or async optionsLoader); accepts values not in the list |
radio | Radio group (single value from options) |
checkbox | Single checkbox (boolean) |
checkbox-group | Multiple checkboxes — “pick any of N” — from options; the model is an array of the checked option values (default the field to []) |
switch-list | A list of labelled toggle rows (allergens, feature flags, opt-ins) from options — each row a real form-grid group with a compact track switch. Same array model as checkbox-group. switchVariant: 'neutral' is usually right for lists (a wall of red-off toggles over-signals); per-row extras via #switch-list-item(<key>) |
currency, percentage | Numeric input with a £/% affix |
image, file | File input (image shows a preview) |
component | Renders your field.component (escape hatch) |
repeater | Nested, repeatable sub-form (see DXRepeater) |
Common options beyond key/type/label:
| Name Click to sort ascending | Type | Required Click to sort ascending | Default | Description |
|---|---|---|---|---|
label / hint | string | (model) => string | No | - | Static or model-derived label and hint text. |
info | string | (model) => string | No | - | PLAIN-TEXT help shown in the label's hover/focus popover — markup does not render, so links and lists are lost. For rich help (links, bulleted lists, bold), use the #info-popover(<key>) slot instead (or #info(<key>) for an always-visible block below the field). |
when | boolean | (model) => boolean | No | true | Conditional visibility, evaluated reactively against the live model. |
readonly | boolean | (model) => boolean | No | false | Render the field read-only — still a bordered, non-editable control. |
plaintext | boolean | (model) => boolean | No | false | Display the value as static text instead of a control (no border, no input box). Implies read-only for every type: anything backed by a real input (text family, textarea, currency, percentage, autocomplete) renders as static text; select/radio/checkbox/switch/file are disabled instead. |
revealable | boolean | No | true | Only for type: "password" — show the reveal (eye) toggle that switches the input between masked and plain text. Set false for a bare password input. |
disabled / disabledWhen | boolean | (model) => boolean | No | false | Disable the field, statically or from the model. |
searchable | boolean | No | false | For a select field: type to filter a long option list while the model still holds option.value (an id). Use for hundreds of options — a plain select is unusable at that size, and the autocomplete type models the typed text, so it cannot back a foreign key |
options / optionsLoader | FieldOption[] | (model) => Promise<FieldOption[]> | No | - | Select/radio options, synchronous or async. Set reloadOptionsOnChange to refetch on model change. |
submit | boolean | No | true | Include the field's value in the submitted payload. Set false for a PRESENTATIONAL field (a header, an alert, an explanatory block via span) that lays the form out but holds no data — DXTable's edit modal seeds its form from every editFields key, so those would otherwise be POSTed alongside the real ones |
span | boolean | No | false | Render full-width, delegating content to the #span(<key>) slot. Also always bypasses horizontal layout's column split. |
layout / labelCols | "vertical" | "horizontal" / number | { sm?, md?, lg?, xl? } | No | - | Per-field override of DXForm's layout/labelCols props. |
currencySymbol / accept / step / min / max | string | number | No | - | Type-specific options (currency symbol, file accept, numeric bounds). |
A FormTab is { key, label?, fieldKeys, when?, lazy? }. Tabs whose when
is false — or that have no visible fields and no custom tab slot — are hidden
automatically.
Extended Component
This is a custom component that extends beyond simple Bootstrap Vue Next wrappers, providing additional functionality specific to Laravel dashboards.