Build a Custom Field
Section: Developer Guide
Topic: Fields
Applies to: Elzo Forms Free and Pro
Scope: PHP, frontend rendering, admin field settings, validation, sanitization
This guide shows how to create a custom Elzo Forms field type. You will build a practical Star Rating field that appears in the form builder, renders on the frontend, saves a submitted rating, supports required validation, and can be used inside JSON form definitions.
What you will build
By the end of this guide, you will have a custom field type called star_rating.
The field will:
- appear in the Elzo Forms field type list;
- render a 1–5 star rating input on the frontend;
- validate that the submitted value is within the allowed rating range;
- sanitize the submitted value before storage;
- add a custom admin setting for the maximum number of stars;
- work inside JSON form data using
"type": "star_rating".
Before you start
This guide assumes you are adding the custom field from a small WordPress plugin or an existing site-specific plugin.
You should not edit Elzo Forms core files directly. Core files can be replaced during plugin updates, and your changes would be lost.
The recommended structure is:
my-elzo-fields/
my-elzo-fields.php
src/
Field_Star_Rating.php
assets/
star-rating.css
How custom fields work in Elzo Forms
An Elzo Forms field type has two main parts:
- A PHP class that extends
\ElzoForms\Field\Field. - A registration callback using the
elzo_forms_field_typesfilter.
The field class controls:
- default field data;
- frontend rendering;
- validation;
- sanitization;
- field-specific admin settings;
- conditional logic metadata when needed.
The registration filter tells Elzo Forms that your custom type exists and which class should handle it.
Step 1: Create the plugin bootstrap file
Create a file at:
wp-content/plugins/my-elzo-fields/my-elzo-fields.php
Add this code:
<?php
/**
* Plugin Name: My Elzo Forms Fields
* Description: Example custom field types for Elzo Forms.
* Version: 1.0.0
* Author: Your Name
*/
defined( 'ABSPATH' ) || exit;
add_action( 'plugins_loaded', function() {
if ( ! class_exists( \ElzoForms\Field\Field::class ) ) {
return;
}
require_once __DIR__ . '/src/Field_Star_Rating.php';
} );
add_filter( 'elzo_forms_field_types', function( array $types ): array {
if ( class_exists( \MyElzoFields\Field_Star_Rating::class ) ) {
$types['star_rating'] = [
'label' => __( 'Star Rating', 'my-elzo-fields' ),
'class' => \MyElzoFields\Field_Star_Rating::class,
];
}
return $types;
} );
This code does two things:
- loads your custom field class after Elzo Forms is available;
- registers the
star_ratingfield type in the form builder.
Step 2: Create the custom field class
Create a file at:
wp-content/plugins/my-elzo-fields/src/Field_Star_Rating.php
Add this code:
<?php
namespace MyElzoFields;
defined( 'ABSPATH' ) || exit;
class Field_Star_Rating extends \ElzoForms\Field\Field {
protected function get_defaults(): array {
return array_merge( parent::get_defaults(), [
'type' => 'star_rating',
'max_rating' => 5,
'default_value' => '',
] );
}
protected function normalize_configuration( array $data ): array {
$max_rating = isset( $data['max_rating'] ) ? absint( $data['max_rating'] ) : 5;
if ( $max_rating < 3 ) {
$max_rating = 3;
}
if ( $max_rating > 10 ) {
$max_rating = 10;
}
$data['max_rating'] = $max_rating;
return $data;
}
public function validate( $value ) {
if ( $value === '' || $value === null ) {
return true;
}
$rating = absint( $value );
$max_rating = absint( $this->get( 'max_rating', 5 ) );
if ( $rating < 1 || $rating > $max_rating ) {
return new \WP_Error(
'invalid_star_rating',
sprintf(
/* translators: 1: field title, 2: maximum rating */
__( 'Please enter a valid rating for %1$s. The rating must be between 1 and %2$d.', 'my-elzo-fields' ),
$this->get_title(),
$max_rating
)
);
}
return true;
}
public function sanitize( $value ) {
if ( $value === '' || $value === null ) {
return '';
}
$rating = absint( $value );
$max_rating = absint( $this->get( 'max_rating', 5 ) );
if ( $rating < 1 ) {
return '';
}
return min( $rating, $max_rating );
}
public function get_data( array $context = [] ): array {
$data = parent::get_data( $context );
$max_rating = absint( $this->get( 'max_rating', 5 ) );
if ( $max_rating < 1 ) {
$max_rating = 5;
}
$data['max_rating'] = $max_rating;
$data['stars'] = range( 1, $max_rating );
$data['value'] = absint( $this->get_value() );
return $data;
}
protected function render_html( array $field_data ): string {
$name = $field_data['name'];
$id = $field_data['id'];
$value = absint( $field_data['value'] );
$stars = $field_data['stars'];
$required = ! empty( $field_data['required'] );
$logic_rules = ! empty( $field_data['logic_rules'] );
ob_start();
?>
<div class="my-elzo-star-rating" role="radiogroup" aria-labelledby="<?php echo esc_attr( $id ); ?>-label">
<span id="<?php echo esc_attr( $id ); ?>-label" class="screen-reader-text">
<?php echo esc_html( $field_data['label'] ?: __( 'Rating', 'my-elzo-fields' ) ); ?>
</span>
<?php foreach ( $stars as $star ) : ?>
<input
type="radio"
name="<?php echo esc_attr( $name ); ?>"
id="<?php echo esc_attr( $id . '-' . $star ); ?>"
value="<?php echo esc_attr( $star ); ?>"
class="my-elzo-star-rating-input"
<?php checked( $value, $star ); ?>
<?php echo $required && ! $logic_rules ? 'required' : ''; ?>
/>
<label
for="<?php echo esc_attr( $id . '-' . $star ); ?>"
class="my-elzo-star-rating-label"
aria-label="<?php echo esc_attr( sprintf( _n( '%d star', '%d stars', $star, 'my-elzo-fields' ), $star ) ); ?>"
>
★
</label>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}
public function render_field_settings( string $tab ): void {
$field = $this->to_array();
$field_id = $this->get_id();
$step_index = $this->get( 'step_index', 0 );
$field_index = $this->get( 'index', 0 );
if ( $tab === 'general' ) {
$max_rating = absint( $this->get( 'max_rating', 5 ) );
?>
<div class="elzo-forms-field-control-group">
<label
for="elzo-forms-field-max-rating-<?php echo esc_attr( $field_id ); ?>"
class="elzo-forms-field-control-label"
>
<?php esc_html_e( 'Maximum rating', 'my-elzo-fields' ); ?>
</label>
<select
name="elzo_form_fields[<?php echo esc_attr( $step_index ); ?>][fields][<?php echo esc_attr( $field_index ); ?>][max_rating]"
id="elzo-forms-field-max-rating-<?php echo esc_attr( $field_id ); ?>"
class="elzo-forms-field-control"
>
<?php for ( $i = 3; $i <= 10; $i++ ) : ?>
<option value="<?php echo esc_attr( $i ); ?>" <?php selected( $max_rating, $i ); ?>>
<?php echo esc_html( $i ); ?>
</option>
<?php endfor; ?>
</select>
</div>
<?php
}
parent::render_field_settings( $tab );
}
}
Normalize field configuration from admin and JSON
The normalize_configuration() method in the example above is important for custom fields that add their own settings.
Elzo Forms runs field configuration normalization in two places:
- when a field object is created from stored form JSON;
- when the WordPress admin builder saves a form, through
\ElzoForms\Field\Field::normalize_definition().
This means your custom field can clean up admin panel input before it is stored. In the Star Rating example, max_rating is converted to an integer and clamped to the supported range of 3 through 10. The same normalization also applies when a form is loaded from JSON.
Use normalize_configuration() for field settings such as limits, modes, default values, option metadata, or any custom keys your field adds to the form definition. Keep it idempotent, avoid database writes or remote calls, and preserve unknown keys unless your field has a specific reason to remove them.
Step 3: Add basic frontend styles
The field works without JavaScript. You can style it with CSS.
Create:
wp-content/plugins/my-elzo-fields/assets/star-rating.css
Add:
.my-elzo-star-rating {
display: inline-flex;
gap: 0.25rem;
}
.my-elzo-star-rating-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.my-elzo-star-rating-label {
cursor: pointer;
font-size: 1.5rem;
line-height: 1;
}
.my-elzo-star-rating-input:checked + .my-elzo-star-rating-label {
font-weight: 700;
}
.my-elzo-star-rating-label:hover,
.my-elzo-star-rating-label:focus {
transform: scale(1.08);
}
Then enqueue the stylesheet from your plugin bootstrap file:
add_action( 'wp_enqueue_scripts', function() {
wp_enqueue_style(
'my-elzo-star-rating',
plugins_url( 'assets/star-rating.css', __FILE__ ),
[],
'1.0.0'
);
} );
This example keeps the CSS intentionally simple. You can improve the visual selected-state behavior with additional CSS or JavaScript if your design requires it.
Step 4: Test the field in the form builder
- Activate your custom plugin.
- Open an Elzo Forms form in the WordPress admin area.
- Add a new field.
- Select Star Rating as the field type.
- Set the label, required state, and maximum rating.
- Save the form.
- Open the form on the frontend and submit a test rating.
If the field does not appear in the field type list, confirm that:
- Elzo Forms is active;
- your custom plugin is active;
- your class file path is correct;
- the class namespace matches the namespace used in
elzo_forms_field_types; - the field type key is unique.
Using the field in JSON form data
Custom field types can be used in JSON form definitions after the field type is registered.
A minimal field definition looks like this:
{
"id": 101,
"type": "star_rating",
"field_key": "customer_rating",
"label": "How would you rate your experience?",
"required": true,
"max_rating": "5"
}
Inside a full JSON form, the field belongs inside a step:
{
"key": "feedback-form",
"title": "Feedback Form",
"status": "publish",
"data": {
"steps": [
{
"label": "Feedback",
"fields": [
{
"id": 101,
"type": "star_rating",
"field_key": "customer_rating",
"label": "How would you rate your experience?",
"required": true,
"max_rating": "5"
}
]
}
],
"settings": {},
"texts": {},
"modules": {}
}
}
Important: JSON files do not register PHP classes. The field type must still be registered in PHP with the elzo_forms_field_types filter before Elzo Forms can render it.
Key Field methods used in this guide
This tutorial intentionally focuses on the methods needed to build one working custom field:
get_defaults()adds default configuration values for the custom field type.normalize_configuration()normalizes field settings from the admin builder and stored JSON.validate()checks submitted values and returnstrueorWP_Error.sanitize()cleans the submitted value before Elzo Forms stores it.get_data()prepares values for rendering.render_html()renders inline field markup when no template file is used.render_field_settings()adds custom controls to the form builder.
For the complete method list, standard configuration keys, JSON format, and related hooks, see Field API Reference.
Validation vs sanitization
Custom fields should use both validation and sanitization.
Validation decides whether the submitted value is acceptable. Return true when it is valid, or return a WP_Error when the form should show an error.
Sanitization cleans the submitted value before storage. It should return the safe value that Elzo Forms can save with the submission.
For the Star Rating field:
validate()makes sure the rating is between1andmax_rating;sanitize()converts the value to an integer and prevents values outside the allowed range.
Conditional logic support
By default, custom fields can use the standard conditional logic operators. If your field stores a simple scalar value, this is usually enough.
For a rating field, you may want numeric operators only:
public function get_logic_operators(): ?array {
return [ '==', '!=', '>', '<' ];
}
If your field has fixed options, you can return them from get_logic_value_options():
public function get_logic_value_options(): array {
$max_rating = absint( $this->get( 'max_rating', 5 ) );
$options = [];
for ( $i = 1; $i <= $max_rating; $i++ ) {
$options[] = [
'value' => (string) $i,
'label' => sprintf(
_n( '%d star', '%d stars', $i, 'my-elzo-fields' ),
$i
),
];
}
return $options;
}
This allows the conditional logic UI to show a dropdown of possible rating values instead of a free text input.
Rendering with a template instead of render_html()
The example above uses render_html() because it keeps the custom field self-contained.
Elzo Forms also supports template-based rendering. For a field type named star_rating, Elzo Forms looks for:
field-types/field-star_rating.php
If you want to provide a template from a custom plugin, use the elzo_forms/templates/locate_template filter to return your template path.
add_filter( 'elzo_forms/templates/locate_template', function( $template, $template_name, $template_path, $args ) {
if ( $template_name === 'field-types/field-star_rating.php' ) {
$custom_template = __DIR__ . '/templates/field-star-rating.php';
if ( file_exists( $custom_template ) ) {
return $custom_template;
}
}
return $template;
}, 10, 4 );
Use a template when your field markup is large or when theme developers need to override it.
Security checklist
When building custom fields, follow these rules:
- Escape every value printed in HTML with
esc_html(),esc_attr(),esc_url(), or another correct escaping function. - Sanitize submitted values inside
sanitize(). - Return
WP_Errorfromvalidate()for invalid values. - Use unique field type keys to avoid conflicts with Elzo Forms core or other plugins.
- Do not store secrets, API keys, or access tokens in field configuration.
- Do not rely only on JavaScript validation. Always validate on the server.
Troubleshooting
The field type does not appear in the builder
Check that your plugin is active, your class file is loaded, and the elzo_forms_field_types filter returns your field definition.
The field renders as empty HTML
Make sure your field class either implements render_html() or provides a valid template path for field-types/field-{type}.php.
The field submits but no value is saved
Check that your input uses the field name from $field_data['name']. Do not hardcode the input name manually.
Required validation does not work as expected
Use the required attribute only when the field is required and does not have conditional logic rules. Server-side validation should still be handled by Elzo Forms and your field class.
JSON forms show the field data but the frontend field does not render
JSON defines the field data only. The PHP field class must still be registered with elzo_forms_field_types.
Related pages
- Field API Reference
- PHP Hooks and Filters Reference
- Template Overrides
- Use JSON Forms
- JavaScript Events API