Claude Skill

wp-admin-form-controls

Use WordPress admin form-control widgets that ship in core,

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lonsdale201-wp-agent-skills-wordpress_wp-admin-form-controls-52f6020.zip · 6 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/wordpress/wp-admin-form-controls
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git git clone https://github.com/Lonsdale201/wp-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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 {
    if ( ! is_string( $value ) ) {
        return '';
    }
    return sanitize_hex_color( $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. Ship a small plugin-owned stylesheet; avoid making wp-admin depend on a third-party CDN. For a simple date-only value, prefer native <input type="date"> and use jQuery UI only when you need a consistent calendar UI or constraints native controls cannot provide.

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 )
    );

    wp_add_inline_script(
        'myplugin-date-init',
        'window.MyPluginDates = ' . wp_json_encode( array(
            'firstDay' => (int) get_option( 'start_of_week', 0 ),
        ) ) . ';',
        'before'
    );
} );

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:        MyPluginDates.firstDay,
        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 onboarding 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.

This is not WordPress 7.1's wp_get_tooltip() / wp_get_toggletip() API. Pointers are dismissible onboarding UI with user-meta persistence; tooltips are accessible control names or supporting context. Reach for that core API when the requested UI is a tooltip/toggletip rather than a one-time tour.

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-picker and wp-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.
  • dateFormat uses jQuery UI's syntax, not PHP's. yy-mm-dd, not Y-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-pointer AJAX action, not a custom one. The user-meta key dismissed_wp_pointers 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).
  • Pointer slugs must be sanitize_key()-safe. Use lowercase letters, numbers, and underscores, or core's dismissal handler rejects the request.
  • Don't init wpColorPicker while 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.
  • WordPress 7.1 bundles jQuery UI 1.14.2 with back-compat enabled. Use public widget APIs; regression-test code that reaches into underscored methods or generated markup.

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-codemirror for the syntax-highlighted textarea variant — different API (wp.codeEditor.initialize) but same general "enqueue + init at DOM-ready" rhythm.
  • See wp-admin-media-frame for the picker that lives next to these on most settings pages.
  • See wp-admin-settings-api for routing the field values through register_setting() + sanitize_callback.
  • See wp-plugin-assets-loading for the $hook_suffix gate that keeps these out of every admin page.

What this skill does NOT cover

  • React/Gutenberg form controls (@wordpress/components Color Picker, Date Picker, etc.). Different API stack — <ColorPicker> not wpColorPicker, 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 — wpColorPicker widget definition with options defaults.
  • wp-includes/js/wp-pointer.js:12 — $.widget('wp.pointer', ...) definition.
  • wp-includes/script-loader.php:1502 — wp-color-picker script handle registration (depends on iris).
  • wp-includes/script-loader.php:860 — wp-pointer script handle (depends on jquery-ui-core).
  • wp-includes/script-loader.php:937-939 — jquery-ui-autocomplete (deps jquery-ui-menu, wp-a11y) and jquery-ui-datepicker (deps jquery-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.
  • Official documentation: https://developer.wordpress.org/reference/functions/wp_enqueue_script/
  • Official documentation: https://api.jqueryui.com/datepicker/
  • Official documentation: https://api.jqueryui.com/autocomplete/
  • Official documentation: https://automattic.github.io/Iris/
Files (wp-agent-skills)
  • reference.md 3.8 KB
    # Admin Form Controls Reference Examples
    
    ## Autocomplete Source Shapes
    
    ```js
    $( '#product' ).autocomplete( {
        source: [ 'apples', 'bananas', 'cherries' ],
        minLength: 1,
    } );
    ```
    
    ```js
    $( '#product' ).autocomplete( {
        source: function ( request, response ) {
            wp.apiFetch( {
                path: `/myplugin/v1/products/search?q=${ encodeURIComponent( request.term ) }`,
            } ).then( ( results ) => {
                response( results.map( ( item ) => ( {
                    label: item.name,
                    value: item.slug,
                    id: item.id,
                } ) ) );
            } ).catch( () => response( [] ) );
        },
        minLength: 2,
        select: function ( event, ui ) {
            $( '#product_id' ).val( ui.item.id );
        },
    } );
    ```
    
    ```js
    $( '#tag' ).autocomplete( {
        source: function ( request, response ) {
            const matches = window.MyPluginTags.filter( ( tag ) =>
                tag.toLowerCase().includes( request.term.toLowerCase() )
            );
            response( matches );
        },
    } );
    ```
    
    ## Pointer Dismissal
    
    ```php
    add_action( 'admin_enqueue_scripts', static function (): void {
        $slug      = 'myplugin_new_dashboard';
        $dismissed = explode( ',', (string) get_user_meta( get_current_user_id(), 'dismissed_wp_pointers', true ) );
    
        if ( in_array( $slug, $dismissed, true ) ) {
            return;
        }
    
        wp_enqueue_script( 'wp-pointer' );
        wp_enqueue_style( 'wp-pointer' );
    
        wp_enqueue_script(
            'myplugin-pointer',
            plugins_url( 'assets/pointer.js', MYPLUGIN_FILE ),
            array( 'wp-pointer', 'wp-i18n' ),
            MYPLUGIN_VERSION,
            array( 'in_footer' => true )
        );
    
        wp_add_inline_script(
            'myplugin-pointer',
            'window.MyPluginPointer = ' . wp_json_encode( array(
                'slug'    => $slug,
                'target'  => '#toplevel_page_myplugin',
                'title'   => __( 'New dashboard', 'myplugin' ),
                'content' => __( 'Check out the new analytics view in the Dashboard tab.', 'myplugin' ),
            ) ) . ';',
            'before'
        );
    } );
    ```
    
    ```js
    jQuery( function ( $ ) {
        const cfg = window.MyPluginPointer;
        if ( ! cfg ) {
            return;
        }
    
        const $target = $( cfg.target );
        if ( ! $target.length ) {
            return;
        }
    
        const content = $( '<div>' ).append(
            $( '<h3>' ).text( cfg.title ),
            $( '<p>' ).text( cfg.content )
        ).html();
    
        $target.pointer( {
            content: content,
            position: { edge: 'left', align: 'center' },
            close: function () {
                $.post( window.ajaxurl, {
                    action: 'dismiss-wp-pointer',
                    pointer: cfg.slug,
                } );
            },
        } ).pointer( 'open' );
    } );
    ```
    
    ## Common Mistakes
    
    ```php
    // WRONG: script without CSS.
    wp_enqueue_script( 'wp-color-picker' );
    
    // RIGHT.
    wp_enqueue_script( 'wp-color-picker' );
    wp_enqueue_style( 'wp-color-picker' );
    ```
    
    ```php
    // WRONG: datepicker with no stylesheet.
    wp_enqueue_script( 'jquery-ui-datepicker' );
    
    // RIGHT.
    wp_enqueue_script( 'jquery-ui-datepicker' );
    wp_enqueue_style( 'myplugin-datepicker', plugins_url( 'assets/datepicker.css', MYPLUGIN_FILE ) );
    ```
    
    ```js
    // WRONG: PHP date() format in jQuery UI.
    $( '#date' ).datepicker( { dateFormat: 'Y-m-d' } );
    
    // RIGHT.
    $( '#date' ).datepicker( { dateFormat: 'yy-mm-dd' } );
    ```
    
    ```js
    // WRONG: returns data instead of calling response().
    $( '#x' ).autocomplete( { source: ( request ) => fetchTags( request.term ) } );
    
    // RIGHT.
    $( '#x' ).autocomplete( {
        source: ( request, response ) => fetchTags( request.term ).then( response ),
    } );
    ```
    
    ```js
    // WRONG: no dismissal persistence.
    $( '#myplugin-menu' ).pointer( { content: '<h3>New!</h3>' } ).pointer( 'open' );
    
    // RIGHT.
    $( '#myplugin-menu' ).pointer( {
        content: '<h3>New!</h3>',
        close: () => $.post( window.ajaxurl, { action: 'dismiss-wp-pointer', pointer: 'myplugin_new_menu' } ),
    } ).pointer( 'open' );
    ```
    
  • SKILL.md 13.7 KB
    ---
    name: wp-admin-form-controls
    description: Use WordPress admin form-control widgets that ship in core,
      `wp-color-picker`, `jquery-ui-datepicker`, `jquery-ui-autocomplete`,
      and `wp-pointer`. Covers correct script/style enqueues, the missing
      jQuery UI datepicker CSS, `wpColorPicker` change / clear callbacks,
      datepicker `yy-mm-dd` formatting plus strict server sanitization,
      autocomplete `source` shapes with `response()`, core user/tag suggest,
      and `wp-pointer` dismissal through `dismiss-wp-pointer`. Use when adding
      color, date, typeahead, or first-run pointer controls to settings pages,
      metaboxes, or repeater rows.
    metadata:
      wp-skills-author: "Soczó Kristóf"
      wp-skills-contact: "mailto:lonsdale201@hotmail.com"
      wp-skills-plugin: "wordpress"
      wp-skills-plugin-version-tested: "6.0 - 7.1"
      wp-skills-wp-version-tested: "7.1"
      wp-skills-php-min: "7.4"
      wp-skills-last-updated: "2026-08-20"
    ---
    
    # 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
    
    ```php
    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
    
    ```php
    <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"
    />
    ```
    
    ```js
    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
    
    ```php
    'sanitize_callback' => static function ( $value ): string {
        if ( ! is_string( $value ) ) {
            return '';
        }
        return sanitize_hex_color( $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**. Ship a small plugin-owned stylesheet; avoid
    making wp-admin depend on a third-party CDN. For a simple date-only value,
    prefer native `<input type="date">` and use jQuery UI only when you need a
    consistent calendar UI or constraints native controls cannot provide.
    
    ### Enqueue
    
    ```php
    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 )
        );
    
        wp_add_inline_script(
            'myplugin-date-init',
            'window.MyPluginDates = ' . wp_json_encode( array(
                'firstDay' => (int) get_option( 'start_of_week', 0 ),
            ) ) . ';',
            'before'
        );
    } );
    ```
    
    If you don't want to bundle your own CSS, the jQuery UI "smoothness" theme CSS works:
    
    ```css
    /* 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
    
    ```php
    <input
        type="text"
        name="myplugin_options[start_date]"
        value="<?php echo esc_attr( $options['start_date'] ?? '' ); ?>"
        class="myplugin-date-field"
        autocomplete="off"
    />
    ```
    
    ```js
    jQuery( function ( $ ) {
        $( '.myplugin-date-field' ).datepicker( {
            dateFormat:      'yy-mm-dd',           // ISO format for storage. NOT PHP's date() format — jQuery UI's.
            firstDay:        MyPluginDates.firstDay,
            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
    
    ```php
    '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
    
    ```php
    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 onboarding 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.
    
    This is not WordPress 7.1's `wp_get_tooltip()` / `wp_get_toggletip()` API.
    Pointers are dismissible onboarding UI with user-meta persistence; tooltips are
    accessible control names or supporting context. Reach for that core API
    when the requested UI is a tooltip/toggletip rather than a one-time tour.
    
    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.
    
    ```php
    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-picker` and `wp-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.
    - **`dateFormat` uses jQuery UI's syntax**, not PHP's. `yy-mm-dd`, not `Y-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-pointer` AJAX action**, not a custom one. The user-meta key `dismissed_wp_pointers` 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).
    - **Pointer slugs must be `sanitize_key()`-safe**. Use lowercase letters, numbers, and underscores, or core's dismissal handler rejects the request.
    - **Don't init `wpColorPicker` while 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.
    - **WordPress 7.1 bundles jQuery UI 1.14.2 with back-compat enabled.** Use public widget APIs; regression-test code that reaches into underscored methods or generated markup.
    
    ## 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-codemirror`** for the syntax-highlighted textarea variant — different API (`wp.codeEditor.initialize`) but same general "enqueue + init at DOM-ready" rhythm.
    - See **`wp-admin-media-frame`** for the picker that lives next to these on most settings pages.
    - See **`wp-admin-settings-api`** for routing the field values through `register_setting()` + `sanitize_callback`.
    - See **`wp-plugin-assets-loading`** for the `$hook_suffix` gate that keeps these out of every admin page.
    
    ## What this skill does NOT cover
    
    - React/Gutenberg form controls (`@wordpress/components` Color Picker, Date Picker, etc.). Different API stack — `<ColorPicker>` not `wpColorPicker`, 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` — `wpColorPicker` widget definition with `options` defaults.
    - `wp-includes/js/wp-pointer.js:12` — `$.widget('wp.pointer', ...)` definition.
    - `wp-includes/script-loader.php:1502` — `wp-color-picker` script handle registration (depends on `iris`).
    - `wp-includes/script-loader.php:860` — `wp-pointer` script handle (depends on `jquery-ui-core`).
    - `wp-includes/script-loader.php:937-939` — `jquery-ui-autocomplete` (deps `jquery-ui-menu`, `wp-a11y`) and `jquery-ui-datepicker` (deps `jquery-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.
    - Official documentation: <https://developer.wordpress.org/reference/functions/wp_enqueue_script/>
    - Official documentation: <https://api.jqueryui.com/datepicker/>
    - Official documentation: <https://api.jqueryui.com/autocomplete/>
    - Official documentation: <https://automattic.github.io/Iris/>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related