{"slug":"wp-admin-form-controls","title":"wp-admin-form-controls","summary":"Use WordPress admin form-control widgets that ship in core,","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:52:22.321032Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-21T19:53:59Z"},"bodyHtml":"<hr>\n<h2>name: wp-admin-form-controls\ndescription: Use WordPress admin form-control widgets that ship in core,\n<code>wp-color-picker</code>, <code>jquery-ui-datepicker</code>, <code>jquery-ui-autocomplete</code>,\nand <code>wp-pointer</code>. Covers correct script/style enqueues, the missing\njQuery UI datepicker CSS, <code>wpColorPicker</code> change / clear callbacks,\ndatepicker <code>yy-mm-dd</code> formatting plus strict server sanitization,\nautocomplete <code>source</code> shapes with <code>response()</code>, core user/tag suggest,\nand <code>wp-pointer</code> dismissal through <code>dismiss-wp-pointer</code>. Use when adding\ncolor, date, typeahead, or first-run pointer controls to settings pages,\nmetaboxes, or repeater rows.\nmetadata:\nwp-skills-author: \"Soczó Kristóf\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"wordpress\"\nwp-skills-plugin-version-tested: \"6.0 - 7.1\"\nwp-skills-wp-version-tested: \"7.1\"\nwp-skills-php-min: \"7.4\"\nwp-skills-last-updated: \"2026-08-20\"</h2>\n<h1>WordPress Admin Form Controls</h1>\n<p>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.</p>\n<h2>When to use this skill</h2>\n<p>Trigger when ANY of the following is true:</p>\n<ul>\n<li>A plugin admin field needs a color picker, a date picker, a typeahead/autocomplete, or a first-run tooltip on a new feature.</li>\n<li>Code references <code>wp-color-picker</code>, <code>wpColorPicker</code>, <code>iris</code>, <code>jquery-ui-datepicker</code>, <code>jquery-ui-autocomplete</code>, <code>wp-pointer</code>, or <code>$('#x').pointer()</code>.</li>\n<li>The user is about to bundle their own color picker (Pickr, color.js, Coloris) or date picker (flatpickr, Pikaday) when core's would do.</li>\n<li>The user complains \"the datepicker has no CSS\" / \"wpColorPicker is not a function\".</li>\n</ul>\n<h2>Color picker — <code>wp-color-picker</code></h2>\n<p>Script handle <code>wp-color-picker</code> depends on <code>iris</code> (the underlying picker — Automattic's color library, <code>wp-includes/js/iris.min.js</code>, registered at <code>wp-includes/script-loader.php:1502</code>). The stylesheet <code>wp-color-picker</code> is also registered; you must enqueue it as well.</p>\n<h3>Enqueue</h3>\n<pre><code>add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void {\n    if ( 'settings_page_myplugin' !== $hook_suffix ) {\n        return;\n    }\n    wp_enqueue_script( 'wp-color-picker' );\n    wp_enqueue_style( 'wp-color-picker' );\n\n    wp_enqueue_script(\n        'myplugin-color-init',\n        plugins_url( 'assets/color-init.js', MYPLUGIN_FILE ),\n        array( 'wp-color-picker', 'wp-i18n' ),\n        MYPLUGIN_VERSION,\n        array( 'in_footer' =&gt; true )\n    );\n} );\n</code></pre>\n<h3>Markup + init</h3>\n<pre><code>&lt;input\n    type=\"text\"\n    name=\"myplugin_options[brand_color]\"\n    value=\"&lt;?php echo esc_attr( $options['brand_color'] ?? '#0073aa' ); ?&gt;\"\n    class=\"myplugin-color-field\"\n    data-default-color=\"#0073aa\"\n/&gt;\n</code></pre>\n<pre><code>jQuery( function ( $ ) {\n    $( '.myplugin-color-field' ).wpColorPicker( {\n        // Optional — picked up automatically from data-default-color if set on the input.\n        // defaultColor: '#0073aa',\n\n        change: function ( event, ui ) {\n            // Fires on every color tweak while the picker is open.\n            // ui.color is an Iris Color object — call .toString() for the hex.\n        },\n        clear: function () {\n            // Fires when the \"Clear\" button is clicked.\n        },\n        palettes: true,                              // false to hide the preset palette row\n        // palettes: [ '#0073aa', '#23282d', '#fff' ], // OR an array of hex strings\n    } );\n} );\n</code></pre>\n<h3>Sanitizing server-side</h3>\n<pre><code>'sanitize_callback' =&gt; static function ( $value ): string {\n    if ( ! is_string( $value ) ) {\n        return '';\n    }\n    return sanitize_hex_color( $value ) ?: '';\n},\n</code></pre>\n<p><code>sanitize_hex_color()</code> returns <code>null</code> on invalid input — coalesce to <code>''</code> (or your default) to keep <code>update_option()</code> happy.</p>\n<h2>Date picker — <code>jquery-ui-datepicker</code></h2>\n<p>The bundled jQuery UI datepicker. The non-obvious bit: <strong>core does NOT enqueue\na default stylesheet for it</strong>. Ship a small plugin-owned stylesheet; avoid\nmaking wp-admin depend on a third-party CDN. For a simple date-only value,\nprefer native <code>&lt;input type=\"date\"&gt;</code> and use jQuery UI only when you need a\nconsistent calendar UI or constraints native controls cannot provide.</p>\n<h3>Enqueue</h3>\n<pre><code>add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void {\n    if ( 'settings_page_myplugin' !== $hook_suffix ) {\n        return;\n    }\n    wp_enqueue_script( 'jquery-ui-datepicker' );\n\n    // CRITICAL — core ships no datepicker CSS. Ship your own:\n    wp_enqueue_style(\n        'myplugin-datepicker',\n        plugins_url( 'assets/datepicker.css', MYPLUGIN_FILE ),\n        array(),\n        MYPLUGIN_VERSION\n    );\n\n    wp_enqueue_script(\n        'myplugin-date-init',\n        plugins_url( 'assets/date-init.js', MYPLUGIN_FILE ),\n        array( 'jquery-ui-datepicker', 'wp-i18n' ),\n        MYPLUGIN_VERSION,\n        array( 'in_footer' =&gt; true )\n    );\n\n    wp_add_inline_script(\n        'myplugin-date-init',\n        'window.MyPluginDates = ' . wp_json_encode( array(\n            'firstDay' =&gt; (int) get_option( 'start_of_week', 0 ),\n        ) ) . ';',\n        'before'\n    );\n} );\n</code></pre>\n<p>If you don't want to bundle your own CSS, the jQuery UI \"smoothness\" theme CSS works:</p>\n<pre><code>/* assets/datepicker.css — minimum the picker needs to be usable */\n.ui-datepicker { background: #fff; border: 1px solid #c3c4c7; padding: 8px; z-index: 9999; }\n.ui-datepicker-header { display: flex; justify-content: space-between; padding: 4px 0; }\n.ui-datepicker-prev, .ui-datepicker-next { cursor: pointer; }\n.ui-datepicker table { border-collapse: collapse; }\n.ui-datepicker td a { display: block; padding: 4px 8px; text-align: center; text-decoration: none; }\n.ui-datepicker td a.ui-state-active { background: #2271b1; color: #fff; }\n</code></pre>\n<h3>Markup + init</h3>\n<pre><code>&lt;input\n    type=\"text\"\n    name=\"myplugin_options[start_date]\"\n    value=\"&lt;?php echo esc_attr( $options['start_date'] ?? '' ); ?&gt;\"\n    class=\"myplugin-date-field\"\n    autocomplete=\"off\"\n/&gt;\n</code></pre>\n<pre><code>jQuery( function ( $ ) {\n    $( '.myplugin-date-field' ).datepicker( {\n        dateFormat:      'yy-mm-dd',           // ISO format for storage. NOT PHP's date() format — jQuery UI's.\n        firstDay:        MyPluginDates.firstDay,\n        changeMonth:     true,\n        changeYear:      true,\n        yearRange:       '-5:+5',\n        showButtonPanel: true,\n    } );\n} );\n</code></pre>\n<p><code>autocomplete=\"off\"</code> on the input prevents the browser from popping its own calendar overlay on top of the jQuery UI one.</p>\n<p><code>dateFormat</code> is jQuery UI's own format string (<code>yy-mm-dd</code> = 4-digit year, 2-digit month, 2-digit day) — NOT PHP's <code>date()</code> syntax. Common confusion source.</p>\n<h3>Sanitizing server-side</h3>\n<pre><code>'sanitize_callback' =&gt; static function ( $value ): string {\n    $raw  = trim( (string) $value );\n    $date = DateTimeImmutable::createFromFormat( '!Y-m-d', $raw );\n    $err  = DateTimeImmutable::getLastErrors();\n\n    if ( ! $date || ( is_array( $err ) &amp;&amp; ( $err['warning_count'] || $err['error_count'] ) ) ) {\n        return '';\n    }\n\n    return $date-&gt;format( 'Y-m-d' ) === $raw ? $raw : '';\n},\n</code></pre>\n<h2>Autocomplete — <code>jquery-ui-autocomplete</code></h2>\n<p>Handle <code>jquery-ui-autocomplete</code> (depends on <code>jquery-ui-menu</code> and <code>wp-a11y</code> — the a11y dep means screen readers get role announcements for free).</p>\n<h3>Enqueue</h3>\n<pre><code>wp_enqueue_script(\n    'myplugin-tag-suggest',\n    plugins_url( 'assets/tag-suggest.js', MYPLUGIN_FILE ),\n    array( 'jquery-ui-autocomplete', 'wp-api-fetch', 'wp-i18n' ),\n    MYPLUGIN_VERSION,\n    array( 'in_footer' =&gt; true )\n);\n</code></pre>\n<h3>Source shapes</h3>\n<p><code>source</code> can be a static array, a synchronous transform, or an async function that calls <code>response( results )</code> after <code>wp.apiFetch()</code>. It cannot just return a Promise. Items can be strings or objects with at least <code>label</code> and <code>value</code>; add an <code>id</code> and read it in <code>select</code> when you need a hidden ID field. See <code>reference.md</code> for complete examples.</p>\n<p>For user / term suggestions, core ships <code>user-suggest</code> (admin pages only) and <code>tags-suggest</code> — those are wrappers around <code>jquery-ui-autocomplete</code> that hit core admin-ajax endpoints. Worth reusing if your \"User\" autocomplete maps to WP users — see <code>wp-admin/js/user-suggest.js</code>.</p>\n<h2>Admin onboarding pointer — <code>wp-pointer</code></h2>\n<p>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.</p>\n<p>This is not WordPress 7.1's <code>wp_get_tooltip()</code> / <code>wp_get_toggletip()</code> API.\nPointers are dismissible onboarding UI with user-meta persistence; tooltips are\naccessible control names or supporting context. Reach for that core API\nwhen the requested UI is a tooltip/toggletip rather than a one-time tour.</p>\n<p>Handle <code>wp-pointer</code> is registered at <code>wp-includes/script-loader.php:860</code> and depends on <code>jquery-ui-core</code>. The matching stylesheet <code>wp-pointer</code> is registered at <code>:1655</code> and depends on <code>dashicons</code> — enqueue both.</p>\n<h3>Dismissal persistence pattern</h3>\n<p>The hard part isn't showing the pointer; it is not showing it again after dismissal. Core stores dismissed pointer slugs in <code>dismissed_wp_pointers</code> user meta. Enqueue <code>wp-pointer</code> + style only when the slug is not already dismissed, then POST <code>{ action: 'dismiss-wp-pointer', pointer: slug }</code> in the pointer <code>close</code> callback. See <code>reference.md</code> for the full safe-content example.</p>\n<h2>Combining multiple controls on one page</h2>\n<p>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.</p>\n<pre><code>wp_enqueue_script( 'wp-color-picker' );\nwp_enqueue_style( 'wp-color-picker' );\nwp_enqueue_script( 'jquery-ui-datepicker' );\nwp_enqueue_style( 'myplugin-datepicker' );\nwp_enqueue_script(\n    'myplugin-fields',\n    plugins_url( 'assets/fields.js', MYPLUGIN_FILE ),\n    array( 'wp-color-picker', 'jquery-ui-datepicker', 'jquery-ui-autocomplete', 'wp-api-fetch', 'wp-i18n' ),\n    MYPLUGIN_VERSION,\n    array( 'in_footer' =&gt; true )\n);\n</code></pre>\n<h2>Critical rules</h2>\n<ul>\n<li><strong>Always enqueue the matching stylesheet</strong> for <code>wp-color-picker</code> and <code>wp-pointer</code>. The script-only enqueue renders unstyled.</li>\n<li><strong>jQuery UI datepicker has NO default WP stylesheet</strong>. You ship one or the picker renders as an ugly unstyled table.</li>\n<li><strong><code>dateFormat</code> uses jQuery UI's syntax</strong>, not PHP's. <code>yy-mm-dd</code>, not <code>Y-m-d</code>. The capitalization differs and silently produces wrong dates.</li>\n<li><strong>Add <code>autocomplete=\"off\"</code> to datepicker / autocomplete inputs</strong> to prevent browser-native overlays from competing with the widget.</li>\n<li><strong>Sanitize server-side regardless of the widget</strong>. The widget is UX, not a validation layer — users can edit the value with DevTools, paste arbitrary text, or disable JS.</li>\n<li><strong>For pointers, use core's <code>dismiss-wp-pointer</code> AJAX action</strong>, not a custom one. The user-meta key <code>dismissed_wp_pointers</code> is 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).</li>\n<li><strong>Pointer slugs must be <code>sanitize_key()</code>-safe</strong>. Use lowercase letters, numbers, and underscores, or core's dismissal handler rejects the request.</li>\n<li><strong>Don't init <code>wpColorPicker</code> while its input is inside a hidden container</strong> — Iris reads computed dimensions at init time. Init AFTER the containing tab/accordion is shown, or call <code>.iris('refresh')</code> on the input after revealing it.</li>\n<li><strong>WordPress 7.1 bundles jQuery UI 1.14.2 with back-compat enabled.</strong> Use public widget APIs; regression-test code that reaches into underscored methods or generated markup.</li>\n</ul>\n<h2>Common AI mistakes</h2>\n<p>See <code>reference.md</code> for before/after snippets: script without stylesheet, unstyled datepicker, PHP date formats in jQuery UI, returning a Promise from autocomplete <code>source</code>, and pointer UI with no dismissal persistence.</p>\n<h2>Cross-references</h2>\n<ul>\n<li>See <strong><code>wp-admin-codemirror</code></strong> for the syntax-highlighted textarea variant — different API (<code>wp.codeEditor.initialize</code>) but same general \"enqueue + init at DOM-ready\" rhythm.</li>\n<li>See <strong><code>wp-admin-media-frame</code></strong> for the picker that lives next to these on most settings pages.</li>\n<li>See <strong><code>wp-admin-settings-api</code></strong> for routing the field values through <code>register_setting()</code> + <code>sanitize_callback</code>.</li>\n<li>See <strong><code>wp-plugin-assets-loading</code></strong> for the <code>$hook_suffix</code> gate that keeps these out of every admin page.</li>\n</ul>\n<h2>What this skill does NOT cover</h2>\n<ul>\n<li>React/Gutenberg form controls (<code>@wordpress/components</code> Color Picker, Date Picker, etc.). Different API stack — <code>&lt;ColorPicker&gt;</code> not <code>wpColorPicker</code>, lives in the block editor or a custom React island.</li>\n<li>Range slider, time picker, file picker. WP doesn't ship dedicated widgets for these in classic admin — for a range, an <code>&lt;input type=\"range\"&gt;</code> works fine; for time, the HTML5 <code>&lt;input type=\"time\"&gt;</code> does the job.</li>\n<li>Customizer color / date controls (<code>wp.customize.ColorControl</code>). Different abstraction over the same picker.</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li><code>wp-admin/js/color-picker.js:23</code> — <code>wpColorPicker</code> widget definition with <code>options</code> defaults.</li>\n<li><code>wp-includes/js/wp-pointer.js:12</code> — <code>$.widget('wp.pointer', ...)</code> definition.</li>\n<li><code>wp-includes/script-loader.php:1502</code> — <code>wp-color-picker</code> script handle registration (depends on <code>iris</code>).</li>\n<li><code>wp-includes/script-loader.php:860</code> — <code>wp-pointer</code> script handle (depends on <code>jquery-ui-core</code>).</li>\n<li><code>wp-includes/script-loader.php:937-939</code> — <code>jquery-ui-autocomplete</code> (deps <code>jquery-ui-menu</code>, <code>wp-a11y</code>) and <code>jquery-ui-datepicker</code> (deps <code>jquery-ui-core</code>).</li>\n<li><code>wp-admin/js/user-suggest.js</code>, <code>wp-admin/js/tags-suggest.js</code> — reference autocomplete implementations for users/tags.</li>\n<li><code>reference.md</code> — autocomplete source shapes, pointer dismissal example, and common mistakes.</li>\n<li>Official documentation: <a href=\"https://developer.wordpress.org/reference/functions/wp_enqueue_script/\">https://developer.wordpress.org/reference/functions/wp_enqueue_script/</a></li>\n<li>Official documentation: <a href=\"https://api.jqueryui.com/datepicker/\">https://api.jqueryui.com/datepicker/</a></li>\n<li>Official documentation: <a href=\"https://api.jqueryui.com/autocomplete/\">https://api.jqueryui.com/autocomplete/</a></li>\n<li>Official documentation: <a href=\"https://automattic.github.io/Iris/\">https://automattic.github.io/Iris/</a></li>\n</ul>\n","files":[{"path":"reference.md","sizeBytes":3928,"isText":true},{"path":"SKILL.md","sizeBytes":14063,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-16T14:58:48.375191Z","sha256":"41FACC4B13D6A129FFCCF051DDA31F725573531B94ADF4AC9CC3BF1672E2FDB8","sizeBytes":6905},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"wordpress/wp-admin-form-controls","license":"MIT","commit":"8820ff3c301066297e696611e3bc4ebeb47d1851","subtreeSha":"38A4EC68775919D02171257886728E6EF831A98283BBCD592EB46429CA98BE3C","lastSyncedAt":"2026-09-22T13:51:11.366991Z"},"reviewedAt":"2026-09-16T15:19:28.671999Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/wordpress/wp-admin-form-controls"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}