Skip to content
Gaurav TiwariDocs

Dynamic options

Version 4.15.1 Updated November 7, 2026

Fill a dropdown, a group of radio buttons or checkboxes, or a text field’s suggestions from your site’s content instead of typing every option by hand. The list stays current: publish a new course, add a category or a new author, and the form shows it the next time it loads.

Core Forms checks every submitted answer against the same list on the server. A visitor can’t edit the page and send a value the list doesn’t offer.

In the Block Editor

Select a Form Choices block and open Options source in the block settings:

Form Choices block with Options source set to Posts or pages, content type and saved value
  • Options I type: the default, a list you type yourself.
  • Posts or pages: published posts of any public content type.
  • Categories, tags or terms: terms of any public taxonomy.
  • Authors: people with published posts.
  • ACF field choices: the choices of an ACF select, radio or checkbox field.
  • Custom field values: every value a custom field has on published content.
  • JSON URL: a list from a JSON file or API.
  • Custom (developer filter): a list your own code supplies.

Saved value picks what submissions, emails and actions store for the chosen option: the title or name (the default), the ID or the slug. Source settings shows the whole source as text. Edit it there to add the settings below.

Preselected value and Placeholder option still work. The placeholder stays the first, empty option.

In HTML forms

Add a data-cf-options attribute:

<label>Course
    <select name="course" data-cf-options="posts:course value=id" required>
        <option value="">Choose a course</option>
    </select>
</label>

Options you type in the HTML stay, and the dynamic ones are added after them. Use that for a placeholder.

For radio buttons or checkboxes, put the attribute on a <fieldset> and name the field with data-cf-name:

<fieldset data-cf-options="terms:topic" data-cf-name="topics" data-cf-input="checkbox" data-cf-required>
    <legend>Topics</legend>
</fieldset>
  • data-cf-input: radio (default) or checkbox. Checkbox values arrive as a list, like any name[] field.
  • data-cf-required: make a choice required.
  • data-cf-selected: preselect values, comma separated. Works on dropdowns too.

Use a <fieldset> for required checkbox groups, so the requirement is checked on the server as well.

For suggestions on a text field, fill a <datalist>. Visitors can still type anything:

<input name="city" list="cities">
<datalist id="cities" data-cf-options="meta:city"></datalist>

Sources

The value has the shape source:argument setting=value setting=value. Settings are separated by spaces, so a value can’t contain one.

Source Argument Settings
posts Post type, default post value=title|id|slug, orderby=title|date|menu_order|modified|id, order=asc|desc, parent=<id>, term=<taxonomy>:<slug or id>[,...], limit
terms Taxonomy, default category value=name|id|slug, parent=<id, slug or name> (0 for top level), hide_empty=1, orderby=name|slug|count|term_order|id, order, limit
users Roles, comma separated (optional) value=name|id, order, limit
acf ACF field key or name None
meta Custom field key post_type, order, limit
json URL (http or https) path=data.items, value=<key>, label=<key>, cache=<seconds>
custom A name you choose Anything; your filter receives them

limit defaults to 100 and can’t go above 500 (filter cf_dynamic_options_max_limit to change that).

What each source will list

  • posts lists published posts of public content types. Drafts, private posts and password-protected posts are never listed.
  • terms lists terms of public taxonomies.
  • users lists only people who have published posts, so a form never reveals accounts that aren’t already public on your site.
  • meta lists values from published content of public content types, and refuses keys that start with an underscore (WordPress’s protected keys).
  • json is fetched by your server, cached for an hour by default (cache= sets 60 seconds to a day), and cached empty for 5 minutes if the request fails. The response must be JSON and under 1 MB. It can be a list of strings, a value: label object, or a list of objects. Use path to reach a nested list, and value/label to name the object keys.

Put {field_name} in the source to fill in another field’s answer. The list stays empty until that field has an answer, then loads in the browser:

Enrollment form where the City list follows the chosen region
<select name="region" data-cf-options="terms:location parent=0" required>
    <option value="">Choose a region</option>
</select>
<select name="city" data-cf-options="terms:location parent={region}" required>
    <option value="">Choose a city</option>
</select>
  • Lists can chain: a third field can depend on city.
  • When the parent changes, the dependent list reloads and keeps its answer if it’s still offered.
  • On submit, the server rebuilds the list with the submitted parent answer, so a city from another region is rejected.
  • With JSON URLs, the answer is URL-encoded: json:https://example.com/states.json?country={country}.

The browser loads these lists from GET /wp-json/core-forms/v1/forms/{id}/options?field=city&values[region]=North. The source always comes from the saved form, so visitors can only supply field answers. The route answers only for published forms, and for logged-in visitors when the form requires a login.

Custom sources

add_filter( 'cf_dynamic_options_custom_plans', function ( $options, $params, $form, $values ) {
    return array(
        'starter' => 'Starter, $9/month',
        'pro'     => 'Pro, $29/month',
    );
}, 10, 4 );
<select name="plan" data-cf-options="custom:plans"></select>

Return a list of strings, a value => label array, or a list of array( 'value' => ..., 'label' => ... ). $params holds the source settings with {field} answers filled in. $values holds those answers.

Filters

  • cf_dynamic_options_custom_{name}: options for custom:{name}.
  • cf_dynamic_options: every resolved list, for any source.
  • cf_dynamic_options_posts_args, cf_dynamic_options_terms_args, cf_dynamic_options_users_args: the query arguments.
  • cf_dynamic_options_allow_post_type, cf_dynamic_options_allow_taxonomy: allow a non-public type, or block a public one.
  • cf_dynamic_options_max_limit: the most options one list may return (500).

Good to know

  • Lists are built each time the form is shown. A page cache keeps showing the list it cached until it’s cleared; dependent lists are always fetched fresh.
  • Submissions store the saved value. If you store IDs, emails show IDs too, so the title is usually the friendlier choice.
  • If a source returns nothing (a typo in the post type, a JSON URL that’s down), the field shows only the options you typed.
Share

Was this helpful?

Still stuck? Open a support ticket