Skip to main content

Customizing Forms

The Dashboard has two different APIs for customizing form inputs. Choose based on what kind of field you are changing:

I want to...Use this APIHow it is selected
Replace the rendered input for a native entity field, such as Product.slugdetailForms[].inputsTarget a specific pageId, blockId, and field
Use my own input for a plugin-defined custom fieldcustomFormComponents.customFieldsRegister a component ID, then reference it from the custom field's ui.component
Use my own input for a configurable operation argumentcustomFormComponents.customFieldsRegister a component ID, then reference it from the argument config's ui.component

Both APIs use the same DashboardFormComponent type, but they are registered differently. Use detailForms[].inputs only when you are replacing a native field on a specific detail page. Use customFormComponents.customFields when the field itself is defined in your plugin configuration.

Shared Component API

All form components must implement the DashboardFormComponent type.

This type is based on the props that are made available from react-hook-form, which is the underlying form library used by the Dashboard.

Component metadata

In addition to the standard React component signature, a dashboard form component can define a static metadata property. This lets the dashboard know how your component should be rendered in certain contexts.

  • isListInput: Declare whether your component is intended for list fields. Use 'dynamic' if it can handle both list and non-list fields.
  • isFullWidth: When true, the dashboard will render the field so it spans the full width of the standard 2-column detail form grids (e.g. DetailFormGrid).
Tsx

Here's an example custom form component that has been annotated to explain the typical parts you will be working with:

src/plugins/my-plugin/dashboard/components/color-picker.tsx

Here's how this component will look when rendered in your form:

Color picker component

Custom Field Components

Use this path for fields defined in your Vendure custom field configuration. Custom field components are registered globally with an ID, then selected from the custom field's ui.component property. They are not targeted by pageId, blockId, or field location.

Let's configure a custom field which uses the ColorPickerComponent as its form component.

First we need to register the component with the defineDashboardExtension function:

src/plugins/my-plugin/dashboard/index.tsx

Now that we've registered it as a custom field component, we can use it in our custom field definition.

src/plugins/my-plugin/my.plugin.ts

Configurable Operation Components

The ColorPickerComponent can also be used as a configurable operation argument component. For example, we can add a color code to a shipping calculator:

src/plugins/my-plugin/config/custom-shipping-calculator.ts

Replacing Native Detail-Page Fields

Use this path when you want to replace the rendered input for a native field on an existing detail page, such as Product.description, Product.slug, or Customer.emailAddress. These overrides are targeted to a specific page, block, and field.

For plugin-defined custom fields, use custom field components instead.

Let's say we want to use a plain text editor for the product description field rather than the default html-based editor.

src/plugins/my-plugin/dashboard/components/markdown-editor.tsx

You can then use this component in your detail form definition:

src/plugins/my-plugin/dashboard/index.tsx

Targeting Input Components

Native detail-page input overrides are targeted in two levels:

  • pageId: The ID of the page, defined on the surrounding detailForms[] entry.
  • blockId: The ID of the form block, defined on each inputs[] item.
  • field: The native field name to replace, defined on each inputs[] item.
Tsx

You can discover the required IDs by turning on dev mode:

Dev mode

and then hovering over any of the form elements will allow you to view the IDs:

Form element IDs

Form Validation

Form validation is handled by the react-hook-form library, which is used by the Dashboard. Internally, the Dashboard uses the zod library to validate the form data, based on the configuration of the custom field or operation argument.

You can access validation data for the current field or the whole form by using the useFormContext hook.

Error Messages

Your component does not need to handle standard error messages - the Dashboard will handle them for you.

For example, if your custom field specifies a pattern property, the Dashboard will automatically display an error message if the input does not match the pattern.

src/plugins/my-plugin/dashboard/components/validated-input.tsx
Best Practices
  1. Always use Shadcn UI components from the @vendure/dashboard package for consistent styling
  2. Handle React Hook Form events properly - call onChange and onBlur appropriately
  3. Display validation errors from fieldState.error when they exist
  4. Use dashboard design tokens - leverage text-destructive, text-muted-foreground, etc.
  5. Provide clear visual feedback for user interactions
  6. Handle disabled states by using the disabled prop
  7. Target native field overrides precisely using pageId, blockId, and field

:::important Design System Consistency Always import UI components from the @vendure/dashboard package rather than creating custom inputs or buttons. This ensures your components follow the dashboard's design system and remain consistent with future updates. :::

Together, custom field components and native detail-page field overrides give you complete control over how data is presented and edited in the Dashboard, while maintaining integration with React Hook Form and the Dashboard design system.

Nested Forms and Event Handling

When creating custom form components that contain their own forms (e.g., dialogs with forms inside detail pages), you need to prevent form submission events from bubbling up to parent forms. The dashboard provides the handleNestedFormSubmit utility for this purpose.

Why Use handleNestedFormSubmit?

Detail pages in the dashboard are themselves forms. If you add a custom component with its own form (like a dialog with create/edit functionality), submitting the inner form will also trigger the outer detail page form submission. This can cause:

  • Unintended save operations on the detail page
  • Validation errors on unrelated fields
  • Loss of unsaved changes in the dialog

Using handleNestedFormSubmit

The handleNestedFormSubmit utility prevents event propagation and properly handles form submission:

src/plugins/my-plugin/dashboard/components/nested-form-dialog.tsx

What handleNestedFormSubmit Does

The utility function:

  1. Prevents the submit event from propagating to parent forms (e.stopPropagation())
  2. Prevents the browser's default form submission behavior (e.preventDefault())
  3. Properly triggers react-hook-form's handleSubmit with your custom handler
  4. Maintains type safety with TypeScript generics

When to Use It

Use handleNestedFormSubmit whenever you have:

  • A dialog with a form inside a detail page
  • A custom component with its own form that's nested within another form
  • Any scenario where form submission events should not bubble up to parent forms

Relation Selector Components

The dashboard includes powerful relation selector components for selecting related entities with built-in search and pagination:

src/plugins/my-plugin/dashboard/components/product-selector.tsx

Features include:

  • Real-time search with debounced input
  • Infinite scroll pagination loading 25 items by default
  • Single and multi-select modes with type safety
  • Customizable GraphQL queries and search filters
  • Built-in UI components using the dashboard design system

Further Reading

For detailed examples of reusable input components, see these dedicated guides:

  • Input component examples - Detailed examples of how to use the APIs available for custom form components.
  • Relation selector components - Build powerful entity selection components with search, pagination, and multi-select capabilities for custom fields and form inputs
Was this chapter helpful?
Report Issue
Edited Jun 24, 2026·Edit this page