WordPress Admin Form Controls
Four small widgets that ship with every WP install and that plugin developers reach for daily — but rarely enqueue correctly. This skill is the recipe sheet.
When to use this skill
Trigger when ANY of the following is true:
- A plugin admin field needs a color picker, a date picker, a typeahead/autocomplete, or a first-run tooltip on a new feature.
- Code references
wp-color-picker,wpColorPicker,iris,jquery-ui-datepicker,jquery-ui-autocomplete,wp-pointer, or$('#x').pointer(). - The user is about to bundle their own color picker (Pickr, color.js, Coloris) or date picker (flatpickr, Pikaday) when core's would do.
- The user complains "the datepicker has no CSS" / "wpColorPicker is not a function".
Color picker — wp-color-picker
Script handle wp-color-picker depends on iris (the underlying picker — Automattic's color library, wp-includes/js/iris.min.js, registered at wp-includes/script-loader.php:1502). The stylesheet wp-color-picker is also registered; you must enqueue it as well.
Enqueue
add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void {
if ( 'settings_page_myplugin' !== $hook_suffix ) {
return;
}
wp_enqueue_script( 'wp-color-picker' );
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script(
'myplugin-color-init',
plugins_url( 'assets/color-init.js', MYPLUGIN_FILE ),
array( 'wp-color-picker', 'wp-i18n' ),
MYPLUGIN_VERSION,
array( 'in_footer' => true )
);
} );
Markup + init
<input
type="text"
name="myplugin_options[brand_color]"
value="<?php echo esc_attr( $options['brand_color'] ?? '#0073aa' ); ?>"
class="myplugin-color-field"
data-default-color="#0073aa"
/>
jQuery( function ( $ ) {
$( '.myplugin-color-field' ).wpColorPicker( {
// Optional — picked up automatically from data-default-color if set on the input.
// defaultColor: '#0073aa',
change: function ( event, ui ) {
// Fires on every color tweak while the picker is open.
// ui.color is an Iris Color object — call .toString() for the hex.
},
clear: function () {
// Fires when the "Clear" button is clicked.
},
palettes: true, // false to hide the preset palette row
// palettes: [ '#0073aa', '#23282d', '#fff' ], // OR an array of hex strings
} );
} );
Sanitizing server-side
'sanitize_callback' => static function ( $value ): string {
return sanitize_hex_color( (string) $value ) ?: '';
},
sanitize_hex_color() returns null on invalid input — coalesce to '' (or your default) to keep update_option() happy.
Date picker — jquery-ui-datepicker
The bundled jQuery UI datepicker. The non-obvious bit: core does NOT enqueue a default stylesheet for it. You either ship your own or pull a CDN one. Plugins frequently render an unstyled datepicker and blame "WordPress weirdness".
Enqueue
add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void {
if ( 'settings_page_myplugin' !== $hook_suffix ) {
return;
}
wp_enqueue_script( 'jquery-ui-datepicker' );
// CRITICAL — core ships no datepicker CSS. Ship your own:
wp_enqueue_style(
'myplugin-datepicker',
plugins_url( 'assets/datepicker.css', MYPLUGIN_FILE ),
array(),
MYPLUGIN_VERSION
);
wp_enqueue_script(
'myplugin-date-init',
plugins_url( 'assets/date-init.js', MYPLUGIN_FILE ),
array( 'jquery-ui-datepicker', 'wp-i18n' ),
MYPLUGIN_VERSION,
array( 'in_footer' => true )
);
} );
If you don't want to bundle your own CSS, the jQuery UI "smoothness" theme CSS works:
/* assets/datepicker.css — minimum the picker needs to be usable */
.ui-datepicker { background: #fff; border: 1px solid #c3c4c7; padding: 8px; z-index: 9999; }
.ui-datepicker-header { display: flex; justify-content: space-between; padding: 4px 0; }
.ui-datepicker-prev, .ui-datepicker-next { cursor: pointer; }
.ui-datepicker table { border-collapse: collapse; }
.ui-datepicker td a { display: block; padding: 4px 8px; text-align: center; text-decoration: none; }
.ui-datepicker td a.ui-state-active { background: #2271b1; color: #fff; }
Markup + init
<input
type="text"
name="myplugin_options[start_date]"
value="<?php echo esc_attr( $options['start_date'] ?? '' ); ?>"
class="myplugin-date-field"
autocomplete="off"
/>
jQuery( function ( $ ) {
$( '.myplugin-date-field' ).datepicker( {
dateFormat: 'yy-mm-dd', // ISO format for storage. NOT PHP's date() format — jQuery UI's.
firstDay: 1, // Monday — or 0 for Sunday
changeMonth: true,
changeYear: true,
yearRange: '-5:+5',
showButtonPanel: true,
} );
} );
autocomplete="off" on the input prevents the browser from popping its own calendar overlay on top of the jQuery UI one.
dateFormat is jQuery UI's own format string (yy-mm-dd = 4-digit year, 2-digit month, 2-digit day) — NOT PHP's date() syntax. Common confusion source.
Sanitizing server-side
'sanitize_callback' => static function ( $value ): string {
$raw = trim( (string) $value );
$date = DateTimeImmutable::createFromFormat( '!Y-m-d', $raw );
$err = DateTimeImmutable::getLastErrors();
if ( ! $date || ( is_array( $err ) && ( $err['warning_count'] || $err['error_count'] ) ) ) {
return '';
}
return $date->format( 'Y-m-d' ) === $raw ? $raw : '';
},
Autocomplete — jquery-ui-autocomplete
Handle jquery-ui-autocomplete (depends on jquery-ui-menu and wp-a11y — the a11y dep means screen readers get role announcements for free).
Enqueue
wp_enqueue_script(
'myplugin-tag-suggest',
plugins_url( 'assets/tag-suggest.js', MYPLUGIN_FILE ),
array( 'jquery-ui-autocomplete', 'wp-api-fetch', 'wp-i18n' ),
MYPLUGIN_VERSION,
array( 'in_footer' => true )
);
Source shapes
source can be a static array, a synchronous transform, or an async function that calls response( results ) after wp.apiFetch(). It cannot just return a Promise. Items can be strings or objects with at least label and value; add an id and read it in select when you need a hidden ID field. See reference.md for complete examples.
For user / term suggestions, core ships user-suggest (admin pages only) and tags-suggest — those are wrappers around jquery-ui-autocomplete that hit core admin-ajax endpoints. Worth reusing if your "User" autocomplete maps to WP users — see wp-admin/js/user-suggest.js.
Admin pointer — wp-pointer
The blue floating tooltip core uses for "new feature" onboarding (e.g. the first-time pointer that introduced the Customizer). Useful in plugins for: announcing a new admin menu item after a version bump, drawing attention to a moved button, first-time-tour-style hints.
Handle wp-pointer is registered at wp-includes/script-loader.php:860 and depends on jquery-ui-core. The matching stylesheet wp-pointer is registered at :1655 and depends on dashicons — enqueue both.
Dismissal persistence pattern
The hard part isn't showing the pointer; it is not showing it again after dismissal. Core stores dismissed pointer slugs in dismissed_wp_pointers user meta. Enqueue wp-pointer + style only when the slug is not already dismissed, then POST { action: 'dismiss-wp-pointer', pointer: slug } in the pointer close callback. See reference.md for the full safe-content example.
Combining multiple controls on one page
The handles compose cleanly — declare them all as deps, init each in DOM-ready. WP loads each only once even if multiple scripts depend on it.
wp_enqueue_script( 'wp-color-picker' );
wp_enqueue_style( 'wp-color-picker' );
wp_enqueue_script( 'jquery-ui-datepicker' );
wp_enqueue_style( 'myplugin-datepicker' );
wp_enqueue_script(
'myplugin-fields',
plugins_url( 'assets/fields.js', MYPLUGIN_FILE ),
array( 'wp-color-picker', 'jquery-ui-datepicker', 'jquery-ui-autocomplete', 'wp-api-fetch', 'wp-i18n' ),
MYPLUGIN_VERSION,
array( 'in_footer' => true )
);
Critical rules
- Always enqueue the matching stylesheet for
wp-color-pickerandwp-pointer. The script-only enqueue renders unstyled. - jQuery UI datepicker has NO default WP stylesheet. You ship one or the picker renders as an ugly unstyled table.
dateFormatuses jQuery UI's syntax, not PHP's.yy-mm-dd, notY-m-d. The capitalization differs and silently produces wrong dates.- Add
autocomplete="off"to datepicker / autocomplete inputs to prevent browser-native overlays from competing with the widget. - Sanitize server-side regardless of the widget. The widget is UX, not a validation layer — users can edit the value with DevTools, paste arbitrary text, or disable JS.
- For pointers, use core's
dismiss-wp-pointerAJAX action, not a custom one. The user-meta keydismissed_wp_pointersis what every other dismissed pointer in WP uses; matching the convention means a clean uninstall (you can remove your slug from the CSV in your uninstaller). - Pointer slugs must be
sanitize_key()-safe. Use lowercase letters, numbers, and underscores, or core's dismissal handler rejects the request. - Don't init
wpColorPickerwhile its input is inside a hidden container — Iris reads computed dimensions at init time. Init AFTER the containing tab/accordion is shown, or call.iris('refresh')on the input after revealing it.
Common AI mistakes
See reference.md for before/after snippets: script without stylesheet, unstyled datepicker, PHP date formats in jQuery UI, returning a Promise from autocomplete source, and pointer UI with no dismissal persistence.
Cross-references
- See
wp-admin-codemirrorfor the syntax-highlighted textarea variant — different API (wp.codeEditor.initialize) but same general "enqueue + init at DOM-ready" rhythm. - See
wp-admin-media-framefor the picker that lives next to these on most settings pages. - See
wp-admin-settings-apifor routing the field values throughregister_setting()+sanitize_callback. - See
wp-plugin-assets-loadingfor the$hook_suffixgate that keeps these out of every admin page.
What this skill does NOT cover
- React/Gutenberg form controls (
@wordpress/componentsColor Picker, Date Picker, etc.). Different API stack —<ColorPicker>notwpColorPicker, lives in the block editor or a custom React island. - Range slider, time picker, file picker. WP doesn't ship dedicated widgets for these in classic admin — for a range, an
<input type="range">works fine; for time, the HTML5<input type="time">does the job. - Customizer color / date controls (
wp.customize.ColorControl). Different abstraction over the same picker.
References
wp-admin/js/color-picker.js:23—wpColorPickerwidget definition withoptionsdefaults.wp-includes/js/wp-pointer.js:12—$.widget('wp.pointer', ...)definition.wp-includes/script-loader.php:1502—wp-color-pickerscript handle registration (depends oniris).wp-includes/script-loader.php:860—wp-pointerscript handle (depends onjquery-ui-core).wp-includes/script-loader.php:937-939—jquery-ui-autocomplete(depsjquery-ui-menu,wp-a11y) andjquery-ui-datepicker(depsjquery-ui-core).wp-admin/js/user-suggest.js,wp-admin/js/tags-suggest.js— reference autocomplete implementations for users/tags.reference.md— autocomplete source shapes, pointer dismissal example, and common mistakes.