Adding a Color Picker Component to Posts & Pages

How to use WordPress components to show a color picker for changing the primary CSS color per page.

Recently, a client had an interesting requirement: They wanted to set the primary (“brand”) color for each page/post. They did showcase multiple projects, and each project page should have a different color.

The website is built with the block editor, and we’re already making heavy use of ACF. Some custom blocks are developed with ACF and some are built in React. I knew that WordPress core has a component for selecting a color – which also powers the color block supports – and wanted to use that in React.

Registering the meta field

First, we have to register a meta field in which to store the value after the user has selected the primary color. Until now, you might not have bothered to register your meta fields, since you could just make up meta keys on the fly. But if you want to show a meta field in the REST API, you have to register it:

/**
 * Register primary color meta so it's available in editor
 */
add_action('init', function () {
    $primaryColorMetaSchema = [
        'single' => true,
        'type' => 'object',
        'show_in_rest' => [
            'name'   => 'primary_color',
            'type'   => 'object',
            'schema' => [
                'type'                 => 'object',
                'context'              => ['edit'],
                'properties'           => [
                    'color' => [
                        'type' => 'string',
                        'format' => 'hex-color',
                        'readonly' => false,
                        'required' => true
                    ],
                    'slug' => [
                        'type' => 'string',
                        'required' => true
                    ],
                    'name' => [
                        'type' => 'string',
                        'required' => true
                    ],
                ],
                'additionalProperties' => false,
            ],
            'default' => [
                'color' => '#3ec16f',
                'slug' => 'green',
                'name' => 'Green'
            ]
        ]
    ];

    register_post_meta('page', 'primary_color', $primaryColorMetaSchema);
    register_post_meta('post', 'primary_color', $primaryColorMetaSchema);
});

Color Picker in the Editor

The search for a pre-built component

I knew, that WordPress core/Gutenberg provides a component with which you can render the color picker that core uses for the color block support. But I didn’t know which component exactly to use – and that required a little bit of digging. I wanted to use the pre-defined theme color palette but also allow users to select a custom value.

This list is accurate as of WordPress 6.2 and therefore until Gutenberg 15.1:

  • ColorPicker
    • A low-level component exported by components package
    • Allows the user to choose a color and manipulating the RGB(A), HSL, HEX color values
  • ColorPalette
    • A low-level exported by the components package
    • Allows the user to choose a predefined value or set a custom value
    • Uses the ColorPicker component for custom values
  • ColorGradientControl
    • Not officially exported by block-editor (only experimental)
    • Not really documented
    • Uses ColorPalette inside
  • ColorPaletteControl
    • Exported by the block-editor package
    • Is not documented
    • Uses ColorGradientControl inside

Initially, I wanted a component which uses the theme color palette, and returns the color and color class name. So pretty much something like the block color support or the withColors higher order component does. But that’s not available as a component, and withColors only works for blocks (since it expects setting the values in attributes).

Therefore, I tried to build my own solution by using ColorPaletteControl. The code for the withColors HOC and the colors block support helped a lot.

Getting and Setting the color in the post meta

import { useEntityProp } from '@wordpress/core-data';

// Get existing meta data
const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
const primaryColor = meta ? meta["primary_color"] : null

// Set handler to set data in metadata
const setPrimaryColor = (value) => {
    setMeta( { ...meta, primary_color: value } );
};

I’m still not really sure about the difference between useEntityProp and getEditedPostAttribute and I find myself often switching between them. I think I’ve seen the great Ryan Welcher use both in his coding streams.

Getting the theme color palette

That’s actually pretty easy:

import { useSetting } from '@wordpress/block-editor';
// Only allow selecting from theme colors
// used for ColorPaletteControl but also for getColorObjectByColorValue
const themeColors = useSetting( 'color.palette.theme' );

Changing the color

ColorPaletteControll’s onChange event returns a color hex code, we need to use WordPress helper functions to get a color object from that. The color object contains a color slug/name (if it’s a registered color from the color palette) and a hex code if it’s a custom color.

import { useCallback } from '@wordpress/element';
import { getColorObjectByColorValue } from '@wordpress/block-editor';

// Callback for ColorPaletteControl - which maps color hex code to color object and saves it
const setColor = useCallback(color => {
  if(color) {
    const colorObject = getColorObjectByColorValue(themeColors, color);
    if(colorObject) {
      setPrimaryColor(colorObject);
    }
  } else {
    setPrimaryColor(null);
  }
}, [themeColors]);

Styling the editor

I also wanted to give the editor a live preview, therefore I wanted to set the changed color in the editor as well:

import { useEffect } from '@wordpress/element';

// Set primary color as css variable on editor styles wrapper to give user some kind of preview in editor.
// Does not set dark/hover colors, but the default color should be enough for editing.
useEffect(() => {
  if(primaryColor) {
    document.querySelector(".editor-styles-wrapper").style.setProperty("--color-primary", primaryColor.color);
  }
}, [primaryColor]);

This may require changing for WordPress 6.3 and block apiVersion 3 to not use document.

The full component

import { registerPlugin } from '@wordpress/plugins';
import {__} from "@wordpress/i18n";
import { useCallback, useEffect } from '@wordpress/element';
import { PluginDocumentSettingPanel } from '@wordpress/edit-post';
import { subscribe, select } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
import { ColorPaletteControl, useSetting, getColorObjectByColorValue } from '@wordpress/block-editor';

const CustomColorPicker = ({postType}) => {
  // Get Post type to use in useEntityProp

  // Get existing meta data
  const [ meta, setMeta ] = useEntityProp( 'postType', postType, 'meta' );
  const primaryColor = meta ? meta["primary_color"] : null

  // Set primary color as css varialbe on editor styles wrapper to give user some kind of preview in editor.
  // Does not set dark/hover colors, but the default color should be enough for editing.
  useEffect(() => {
    if(primaryColor) {
      document.querySelector(".editor-styles-wrapper").style.setProperty("--color-primary", primaryColor.color);
    }
  }, [primaryColor]);

  // Set handler to set data in metadata
  const setPrimaryColor = (value) => {
    setMeta( { ...meta, primary_color: value } );
  };

  // Only allow selecting from theme colors
  // used for ColorPaletteControl but also for getColorObjectByColorValue
  const themeColors = useSetting( 'color.palette.theme' );

  // Callback for ColorPaletteControl - which maps color hex code to color object and saves it
  const setColor = useCallback(color => {
    if(color) {
      const colorObject = getColorObjectByColorValue(themeColors, color);
      if(colorObject) {
        setPrimaryColor(colorObject);
      }
    } else {
      setPrimaryColor(null);
    }
  }, [themeColors]);

  // Render ColorPaletteControl
  return (
    <ColorPaletteControl
        label={__("Primary color", "my-custom-colors")}
        value={ primaryColor ? primaryColor.color : null }
        onChange={ setColor }
        disableCustomColors
        gradients={[]}
        disableCustomGradients
        color={themeColors}
    />
  )
};

Registering the sidebar panel for posts & pages

To show the panel in the document sidebar, we need to register it via registerPlugin.

I wanted to show the panel only on posts and pages. I could’ve just enqueued the script only on these post types, but our build process builds a single editor.js file, therefore I had to check for the post type in JavaScript. That’s a little bit tricky because you have to get the post type from the Redux store, which may not immediately return a value – therefore we have to subscribe to that select and then unsubscribe.

/**
 * only register plugin for page+post post type
 * use subscribe because post type may not be set when using select directly on domReady
 * after the post type is set the first time we can unsubscribe (post type won't change anymore)
 *
 * TODO: monitor if performance problematic?
 */
const unsubscribePostTypeListener = subscribe(() => {
  const postType = select("core/editor").getCurrentPostType();
  if (postType !== null) {
    unsubscribePostTypeListener();
  } else {
    return;
  }
  if(["page", "post"].includes(postType)) {
    registerPlugin("my-custom-colors", {
      render: () => {
        return (
        <PluginDocumentSettingPanel
          name="my-custom-colors"
          title={__("Custom Colors", "my-custom-colors")}
          className="my-custom-colors"
          >
            <CustomColorPicker postType={postType} />
        </PluginDocumentSettingPanel>);
      },
      icon: "color"
    })
  }
});

Frontend

Add the color class to the body

I’ve decided to add the primary color as a class to the body and then change the primary color CSS variable depending on that:

/**
 * Add primary color to body class
 */
add_filter('body_class', function ($classes) {
    $postType = get_post_type();
    if ($postType === 'page' || $postType === 'post') {
        $primaryColor = get_post_meta(get_the_ID(), 'primary_color', true);
        if ($primaryColor && is_array($primaryColor) && isset($primaryColor['slug'])) {
            $classes[] = esc_attr('has-' . $primaryColor['slug'] . '-primary-color');
        }
    }
    return $classes;
});

Set primary color depending on color class

We’re actually using SCSS for our styles, so creating those variations was actually pretty easy:

@each $color, $value in $theme-colors {
  body.has-#{$color}-primary-color {
    --color-primary: #{$value};
  }
}

In our blocks, modules and other styles we then just use var(--color-primary). It’s also used for links and buttons.

Conclusion

So that’s my solution for allowing editors to change the primary color for pages.

WordPress block editor (aka Gutenberg) provide a lot of components which make building thins like this really easy. But because of missing documentation, it’s not always clear which one to use – especially if there are a lot of components which seem to do the same.

It really paid off that we used CSS variables in the frontend – this way the frontend changes were minimal and only required referencing the root CSS variable which is changed on the body.

For the same client we also built a system to allow adding color pickers to custom blocks (especially ACF blocks) which use the React components and not ACFs color picker. That was before the color block support was official. If you’re interested in that, I can also write that up.

Have you built something similar and used WordPress components? What were your experiences and which components and hooks did you use?