WordPress 5.8 Duotone Image Filter: Register Custom Presets and Apply to Image and Cover Blocks

WordPress 5.8 introduced a duotone filter system for images — a design technique that renders an image using exactly two colours, one for shadows and one for highlights. The Duotone filter is applied as an SVG filter element and is registered declaratively in theme.json under the color.duotone array. Registered duotones appear in the Image and Cover block toolbars automatically. Themes can define a custom duotone palette, and plugins can add duotone presets programmatically via the block_editor_settings_all filter. The implementation uses an inline SVG <filter> element combined with CSS filter: url(#wp-duotone-...) on the image wrapper — understanding this mechanism is useful for debugging duotone rendering issues and for extending the system with custom CSS.

Problem: A photography portfolio theme needs a set of branded duotone presets (navy+gold, teal+cream) available site-wide in all Image and Cover blocks, without modifying the theme's theme.json at the filesystem level — the presets must be added by a plugin for maintainability.

Solution: Use the block_editor_settings_all filter to append custom duotone presets to the editor settings array. Each preset needs a slug, name, and a colors array of exactly two hex values (shadow colour first, highlight colour second).

<?php
// ── Register custom duotone presets via plugin ────────────────────────
add_filter( 'block_editor_settings_all', function ( array $settings ): array {
    $custom_duotones = [
        [
            'slug'   => 'navy-gold',
            'name'   => __( 'Navy + Gold', 'textdomain' ),
            'colors' => [ '#0a2342', '#f5c842' ], // shadow, highlight
        ],
        [
            'slug'   => 'teal-cream',
            'name'   => __( 'Teal + Cream', 'textdomain' ),
            'colors' => [ '#0d7377', '#f5f0e8' ],
        ],
        [
            'slug'   => 'charcoal-white',
            'name'   => __( 'Charcoal + White', 'textdomain' ),
            'colors' => [ '#2c2c2c', '#ffffff' ],
        ],
    ];

    // Merge with any existing duotones (from theme.json or other plugins)
    $existing = $settings['__experimentalFeatures']['color']['duotone'] ?? [];
    $settings['__experimentalFeatures']['color']['duotone'] = array_merge(
        $existing,
        $custom_duotones
    );

    return $settings;
} );

// ── Equivalent theme.json definition (for block themes) ──────────────
// In theme.json, add under settings.color.duotone:
// {
//   "settings": {
//     "color": {
//       "duotone": [
//         { "slug": "navy-gold", "name": "Navy + Gold", "colors": ["#0a2342","#f5c842"] },
//         { "slug": "teal-cream", "name": "Teal + Cream", "colors": ["#0d7377","#f5f0e8"] }
//       ]
//     }
//   }
// }

// ── How the front-end output looks (generated by WordPress) ───────────
// WordPress outputs an SVG filter in <head> like this:
// <svg xmlns="..." style="display:none" aria-hidden="true">
//   <filter id="wp-duotone-navy-gold">
//     <feColorMatrix type="saturate" values="0"/>
//     <feColorMatrix type="matrix" values="r1 0 0 0 r2  g1 0 0 0 g2  ..."/>
//   </filter>
// </svg>
// The block wrapper then gets: style="filter:url(#wp-duotone-navy-gold)"

// ── Disable duotone on specific blocks if performance is a concern ────
add_filter( 'block_type_metadata_settings', function ( array $settings, array $metadata ): array {
    // Disable duotone support on the Cover block
    if ( 'core/cover' === ( $metadata['name'] ?? '' ) ) {
        $settings['supports']['color']['duotone'] = false;
    }
    return $settings;
}, 10, 2 );

NOTE: Each duotone preset generates an SVG <filter> element in the page <head> — even if it is not used on that specific page. On sites with many presets this adds to the HTML payload. WordPress caches the SVG output, but if you register many duotones (more than 10–12), audit whether all are actually used across the site. The colors array must contain exactly two values: the first is mapped to dark/shadow pixels and the second to light/highlight pixels. Duotone is applied as a CSS filter property, which means it affects all child elements of the wrapper, not just the <img> tag — this can cause unexpected colouring of overlaid text in Cover blocks.