Custom Actions & Progress
Replace the built-in form buttons and step progress bar with your own components. Useful when your project has a design system and the default unstyled HTML doesn't fit.
Custom actions component
Pass your component via the actionsComponent prop:
<script setup lang="ts">
import { FormRenderer } from '@formhaus/vue'
import MyFormActions from './MyFormActions.vue'
</script>
<template>
<FormRenderer
:definition="definition"
:actions-component="MyFormActions"
@submit="onSubmit"
/>
</template>import { FormRenderer } from '@formhaus/react'
import { MyFormActions } from './MyFormActions'
<FormRenderer
definition={definition}
actionsComponent={MyFormActions}
onSubmit={handleSubmit}
/>Your component receives pre-computed convenience props so you don't need to re-derive button labels or visibility:
| Prop | Type | Description |
|---|---|---|
primaryLabel | string? | Resolved label for the primary button ("Continue" or submit label) |
showPrimary | boolean? | False when the current step hides Next with next: false; final Submit remains visible |
showBack | boolean? | Whether the back button should be shown |
backLabel | string? | Resolved label for the back button |
showSkip | boolean? | Whether the skip button should be shown |
skipLabel | string? | Label for the skip button, defaults to the skip label or "Skip" |
loading | boolean? | Parent loading state or pending navigation/submission, including hooks |
cancelAction | FormAction? | Cancel button config |
In React, call onPrimary for the primary action, onPrev for back and onSkip for skip. In Vue, emit primary, prev and skip.
Vue example
<script setup lang="ts">
import type { FormActionsProps } from '@formhaus/vue'
defineProps<FormActionsProps>()
const emit = defineEmits<{
(e: 'primary'): void
(e: 'prev'): void
(e: 'cancel'): void
}>()
</script>
<template>
<div class="my-actions">
<button v-if="showBack" type="button" :disabled="loading" @click="emit('prev')">{{ backLabel }}</button>
<button v-if="showPrimary !== false" type="button" @click="emit('primary')" :disabled="loading">{{ primaryLabel }}</button>
</div>
</template>React example
import type { FormActionsProps } from '@formhaus/react'
export function MyFormActions({
primaryLabel, showPrimary = true, showBack, backLabel, loading,
onPrimary, onPrev,
}: FormActionsProps) {
return (
<div className="my-actions">
{showBack && <button type="button" disabled={loading} onClick={onPrev}>{backLabel}</button>}
{showPrimary && <button type="button" disabled={loading} onClick={onPrimary}>{primaryLabel}</button>}
</div>
)
}Raw props
| Prop | Type | Description |
|---|---|---|
submitAction | FormAction? | Label and variant for the submit button |
backAction | FormAction | false? | Back button config, or false to hide |
cancelAction | FormAction? | Cancel button config |
skipAction | FormAction? | Skip button config of the current step |
isFirstStep | boolean | Whether this is the first step |
isLastStep | boolean | Whether this is the last step |
isMultiStep | boolean | Whether the form has multiple steps |
loading | boolean? | Parent loading state or pending navigation/submission, including hooks |
In Vue, you can also emit submit, next, prev, cancel events directly. In React, call onSubmit, onNext, onPrev, onCancel callbacks.
Custom progress component
Pass your component via the progressComponent prop:
<template>
<FormRenderer
:definition="definition"
:progress-component="MyStepProgress"
@submit="onSubmit"
/>
</template><FormRenderer
definition={definition}
progressComponent={MyStepProgress}
onSubmit={handleSubmit}
/>Your component receives these props:
| Prop | Type | Description |
|---|---|---|
current | number | Current step number (1-based) |
total | number | Total visible steps |
stepTitle | string? | Title of the current step |
stepDescription | string? | Description of the current step |
headingLevel | 1 to 6, optional | Heading level for the step title, from the renderer's headingLevel prop. The built-in progress renders <h2> by default |
The progress component only renders for multi-step forms.
Combining both
Use all custom component props together:
<template>
<FormRenderer
:definition="definition"
:components="{ phone: CustomPhoneInput }"
:actions-component="MyFormActions"
:progress-component="MyStepProgress"
@submit="onSubmit"
/>
</template><FormRenderer
definition={definition}
components={{ phone: CustomPhoneInput }}
actionsComponent={MyFormActions}
progressComponent={MyStepProgress}
onSubmit={handleSubmit}
/>If you don't pass these props, the built-in components render as before. No breaking changes.
Headless renderer
FormRenderer imports the built-in native fields, actions and step progress. They stay in your bundle even when you replace all of them. If you map every field type you use and pass your own actions and progress components, use HeadlessFormRenderer. It takes the same props and events as FormRenderer, but it does not import any built-in component. This drops the built-in fields from your bundle.
<script setup lang="ts">
import { HeadlessFormRenderer } from '@formhaus/vue'
</script>
<template>
<HeadlessFormRenderer
:definition="definition"
:components="{ text: MyInput, email: MyInput, select: MySelect }"
:actions-component="MyFormActions"
:progress-component="MyStepProgress"
@submit="onSubmit"
/>
</template>import { HeadlessFormRenderer } from '@formhaus/react'
<HeadlessFormRenderer
definition={definition}
components={{ text: MyInput, email: MyInput, select: MySelect }}
actionsComponent={MyFormActions}
progressComponent={MyStepProgress}
onSubmit={handleSubmit}
/>HeadlessFormRenderer supports the same lifecycle hooks and awaited submission. Custom fields must implement explicit activation for autoAdvance; custom actions must honor showPrimary and loading as above.
HeadlessFormRenderer has no fallbacks:
- A field type with no mapped component renders an "Unsupported field type" placeholder.
- Without an actions component, the form renders no buttons.
- Without a progress component, a multi-step form renders no progress bar.
Render one field
FormhausField renders a single field with the built-in components as fallbacks. Pass components to override some types. Both packages also export every built-in field (TextField, TextareaField, SelectField, AutocompleteField, MultiselectField, CheckboxField, RadioField, SwitchField, FileField, DateField, DateTimeField) and defaultFieldComponents.
<FormhausField :field="field" v-model="value" /><FormhausField field={field} value={value} onChange={setValue} onBlur={() => {}} />Exported types
Import the prop interfaces to type your custom components:
// Vue
import type { FormActionsProps, FormStepProgressProps } from '@formhaus/vue'
// React
import type { FormActionsProps, FormStepProgressProps } from '@formhaus/react'Analytics events
Track form interactions via onAnalyticsEvent (React) or @analyticsEvent (Vue). Event type names are snake_case, the usual convention for analytics tools:
<FormRenderer
definition={definition}
onAnalyticsEvent={(event) => analytics.track(event.type, event)}
onSubmit={handleSubmit}
/><FormRenderer
:definition="definition"
@analytics-event="(event) => analytics.track(event.type, event)"
@submit="onSubmit"
/>Events emitted:
| Event | When | Payload |
|---|---|---|
field_focused | User focuses a field | fieldKey |
field_blurred | User leaves a field | fieldKey, hasValue |
field_error | Validation fails | fieldKey, error |
step_completed | User advances past a step | stepId |
step_skipped | User skips a step | stepId |
step_viewed | A step becomes active | stepId, stepIndex |
form_submitted | Validation and onBeforeSubmit pass, right before the submit handler runs | fieldCount |
form_submitted marks a submission attempt, not a successful save. Track the save in onAfterSubmit. All events are optional. If you don't pass a handler, nothing fires.
Next steps
- Field Types: customize field components via the
componentsprop - Multi-Step Forms: step navigation, conditional steps, progress
- Examples: full form patterns