Skip to content

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:

vue
<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>
tsx
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:

PropTypeDescription
primaryLabelstring?Resolved label for the primary button ("Continue" or submit label)
showPrimaryboolean?False when the current step hides Next with next: false; final Submit remains visible
showBackboolean?Whether the back button should be shown
backLabelstring?Resolved label for the back button
showSkipboolean?Whether the skip button should be shown
skipLabelstring?Label for the skip button, defaults to the skip label or "Skip"
loadingboolean?Parent loading state or pending navigation/submission, including hooks
cancelActionFormAction?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 ​

vue
<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 ​

tsx
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 ​

PropTypeDescription
submitActionFormAction?Label and variant for the submit button
backActionFormAction | false?Back button config, or false to hide
cancelActionFormAction?Cancel button config
skipActionFormAction?Skip button config of the current step
isFirstStepbooleanWhether this is the first step
isLastStepbooleanWhether this is the last step
isMultiStepbooleanWhether the form has multiple steps
loadingboolean?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:

vue
<template>
  <FormRenderer
    :definition="definition"
    :progress-component="MyStepProgress"
    @submit="onSubmit"
  />
</template>
tsx
<FormRenderer
  definition={definition}
  progressComponent={MyStepProgress}
  onSubmit={handleSubmit}
/>

Your component receives these props:

PropTypeDescription
currentnumberCurrent step number (1-based)
totalnumberTotal visible steps
stepTitlestring?Title of the current step
stepDescriptionstring?Description of the current step
headingLevel1 to 6, optionalHeading 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:

vue
<template>
  <FormRenderer
    :definition="definition"
    :components="{ phone: CustomPhoneInput }"
    :actions-component="MyFormActions"
    :progress-component="MyStepProgress"
    @submit="onSubmit"
  />
</template>
tsx
<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.

vue
<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>
tsx
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.

vue
<FormhausField :field="field" v-model="value" />
tsx
<FormhausField field={field} value={value} onChange={setValue} onBlur={() => {}} />

Exported types ​

Import the prop interfaces to type your custom components:

ts
// 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:

tsx
<FormRenderer
  definition={definition}
  onAnalyticsEvent={(event) => analytics.track(event.type, event)}
  onSubmit={handleSubmit}
/>
vue
<FormRenderer
  :definition="definition"
  @analytics-event="(event) => analytics.track(event.type, event)"
  @submit="onSubmit"
/>

Events emitted:

EventWhenPayload
field_focusedUser focuses a fieldfieldKey
field_blurredUser leaves a fieldfieldKey, hasValue
field_errorValidation failsfieldKey, error
step_completedUser advances past a stepstepId
step_skippedUser skips a stepstepId
step_viewedA step becomes activestepId, stepIndex
form_submittedValidation and onBeforeSubmit pass, right before the submit handler runsfieldCount

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 ​

Formhaus is MIT licensed. Legal