Developer Hook Reference

This page is for developers who extend Canvas from a child theme or a plugin. It lists the filters and actions Canvas keeps stable, shows how to add a setting to a Canvas block, and covers the wp canvas commands, the demo import ability and what a child theme can override. Everything here is read from the running code, so a hook that is not on this page is internal and may change without notice.

Put the examples in your child theme's functions.php or in a small plugin. Most hooks can be added from either. The few that run while plugins are still loading are marked, and those need a plugin or a must-use plugin, because a theme's functions.php loads after them.

How To Add A Field To A Canvas Block

Two filters let you change the settings of a Canvas block without copying the block:

  • canvas_block_fields (filter) Receives $fields (array), $name (string, the block name without its namespace, such as icon-box) and $block (the block instance). Runs for every Canvas block.
  • canvas_block_{name}_fields (filter) The same for one block, for example canvas_block_icon-box_fields. Receives $fields and $block.

A field uses the same definition format as a Theme Options field, keyed by its id: type, label, default, and where they apply choices, description and condition. Give it a panel too, so it lands in a named panel of the sidebar rather than in the block's general settings.

A field added here is handled like one the block declares itself: it is registered as a block attribute, sanitised by its type when the block renders, shown in the block's sidebar, and passed to the block's component template as $args['<field id>']. Add the callback before init, because block attributes are registered then; a child theme's functions.php is early enough.

This example adds an Emphasis setting to the Icon Box block and turns its Shadow choice into a class on the box:

add_filter( 'canvas_block_icon-box_fields', function ( $fields ) {
	$fields['emphasis'] = array(
		'type'    => 'select',
		'label'   => __( 'Emphasis', 'canvas-child' ),
		'choices' => array(
			''       => __( 'None', 'canvas-child' ),
			'shadow' => __( 'Shadow', 'canvas-child' ),
		),
		'default' => '',
		'panel'   => 'Content',
	);

	return $fields;
} );

add_filter( 'canvas_component_props', function ( $props, $component ) {
	if ( 'icon-box' === $component && 'shadow' === ( $props['emphasis'] ?? '' ) ) {
		$props['css_class'] = trim( ( $props['css_class'] ?? '' ) . ' shadow' );
	}

	return $props;
}, 10, 2 );

A callback must return an array. Anything else is ignored with a notice, and the block keeps its own fields.

The Theme Support Contract

Canvas declares, in its theme setup:

add_theme_support( 'canvas-core', array( 'api' => 1, 'edition' => 'bootstrap' ) );

The declaration tells Canvas Core that the active theme renders the Canvas block contract. While it holds, the plugin activates without refusing the theme, shows no "requires the Canvas theme" notice, allows demo imports, and keeps the WordPress site logo and the Canvas logo setting in step. Canvas itself, and any child theme of Canvas, is recognised by name as well, so a child theme needs no declaration of its own.

Today the plugin reads only whether the support is declared. The api and edition values record which version of the contract the theme renders and which markup edition it ships; nothing reads them yet, so declare the values Canvas declares. The declaration does not supply what the blocks render through: Canvas blocks call the theme's canvas_render() and its components/ templates, so a theme that is not Canvas must provide those as well.

Component Templates

Canvas blocks render through the theme's component templates with canvas_render( $component, $props ). These hooks run around every component render:

  • canvas_locate_component (filter) Receives $template_path (false), $component (string) and $props (array). Return an absolute path to render that file instead of the theme's template.
  • canvas_component_paths (filter) Receives $paths (the theme relative candidates, components/{name}.php then template-parts/{name}.php) and $component. The list is resolved with locate_template(), so a child theme's copy wins.
  • canvas_component_props (filter) Receives $props and $component. Change the values a template receives, as the example above does.
  • canvas_component_output (filter) Receives $output (string), $component and $props. Change the rendered markup.
  • canvas_before_render and canvas_after_render (actions) Receive $component and $props; the after action also receives $output.

These hooks shape the header, footer and the bands around the content:

  • canvas_header_classes (filter) Receives $classes (array) and $config (array, the resolved header settings). Returns the classes of the <header> element. It runs for a header built in the Header Builder and for a Canvas Header Root block with Use Theme Options on.
  • canvas_header_data (filter) Receives $data (array of data-* attribute name => value) and $config. The attributes the header script reads, such as data-sticky-class and data-responsive-class. Per page settings only reach a single page, so this is how a design sets the header of an archive it owns.
  • canvas_header_underlay (filter) Receives $has (bool) and $post_id (int, 0 when not singular). Return true when your code draws a full bleed band under the header on canvas_after_header, so a transparent header keeps its transparent scheme.
  • canvas_header_suppressed (filter) Receives $suppressed (bool), $config and $post_id. Return true to emit no header at all. Content on canvas_after_header still renders.
  • canvas_top_bar_suppressed (filter) Receives $suppressed and $post_id. Return true to emit no top bar of either kind. The header itself is unaffected.
  • canvas_header_after_blocks (filter) Receives $names (array of block names, canvas-core/app-menu by default). Top level blocks of a built header with these names are printed after </header> instead of inside it.
  • canvas_before_header and canvas_after_header (actions) No arguments. Echo markup and it is placed immediately before, or after, the site header, once per page. Canvas prints the page slider at priority 5, the hero at 6 and the page title band at 10 on canvas_after_header.
  • canvas_footer_suppressed (filter) Receives $suppressed (bool) and $post_id. Return true to emit no footer at all.
  • canvas_footer_classes (filter) Receives $classes (array) and $args (array). The classes of the <footer> element Canvas wraps around a Footer record. A record whose blocks already include their own <footer>, as the footer patterns do, keeps that element and its classes.
  • canvas_footer_before_content and canvas_footer_after_content (actions) Receive $post_id (the Footer record) and $args. Fire around the content of a Footer record written without blocks, which Canvas wraps in its own footer chrome. A record built from blocks renders as it is, without them.
  • canvas_footer_markup (filter) Receives $footer_markup (string) and $post_id (the Footer record). The rendered markup of a Footer record built from blocks, before it is printed. Canvas uses it to put an assigned Copyrights record in place of the footer's own copyright bar.
  • canvas_copyrights_before_content and canvas_copyrights_after_content (actions) Receive $post_id (the Copyrights record) and $args. Fire inside the copyright bar Canvas prints for an assigned Copyrights record, in any footer, around the record's content.
  • canvas_footer_copyright_text (filter) Receives $text (string). The copyright line the default footer and an empty Canvas Copyrights block print. The [canvas_year] shortcode inside it is expanded after this filter, so you can return text containing it.
  • canvas_side_panel_classes (filter) Receives $classes (array) and $args. The classes of the side panel.
  • canvas_page_title_text and canvas_page_title_subtitle (filters) Receive the title or subtitle (string) of the page title band.
  • canvas_page_title_resolved_settings (filter) Receives $settings (array, the fully resolved page title settings) and $post_id (0 on archives).
  • canvas_page_title_classes (filter) Receives $classes (array) and $settings. The classes of the page title band.
  • canvas_breadcrumbs_html (filter) Receives null and $post_id. Return a string to replace the whole trail the Page Title Breadcrumbs block draws.

Canvas adds its own callbacks to canvas_header_classes, one of them at priority 99: it swaps a transparent header to its solid scheme on a page with nothing under the bar. A transparent class added after priority 99 bypasses that safeguard, so declare an underlay with canvas_header_underlay instead.

Per Page Settings

The page settings panels, Header Settings, Page Title Settings, Page Layout and the rest, are registered for a list of post types, and each list is filterable. Every filter receives and returns an array of post type names:

  • canvas_chrome_meta_post_types The base list for Header Settings, Page Title - Layout & Media and Page Layout: every post type that is viewable, has an admin screen and supports the editor.
  • canvas_header_meta_post_types, canvas_page_title_layout_post_types and canvas_page_layout_post_types The final list for each of those three panels.
  • canvas_page_title_post_types Page Title Settings. Defaults to every public post type with an admin screen, except attachments.
  • canvas_slider_meta_post_types Slider. Defaults to every public post type except sliders, slides and attachments.
  • canvas_hero_meta_post_types, canvas_footer_meta_post_types, canvas_copyrights_meta_post_types and canvas_side_panel_meta_post_types Hero, Footer, Copyrights and Side Panel. Default to pages and posts.
  • canvas_seo_meta_post_types Canvas SEO. Defaults to posts, pages and products.

A post type on one of these lists gets the settings as a panel in the block editor when it is shown in the REST API, uses the block editor, and supports custom-fields; posts and pages always qualify. Otherwise it gets the same settings as a classic box around the editor. Register your type with 'show_in_rest' => true and custom-fields in supports if you want the panels. Add these filters before init priority 20, when the panels register.

Theme Options

These hooks read, write and extend the site wide settings:

  • canvas_register_options_sections and canvas_register_option_fields (actions) Receive the Canvas_Core\Options\Fields instance. Call its register_section( $id, $settings ) and register_field( $id, $settings ) to add your own Theme Options tab and settings.
  • canvas_option_{$key} (filter) Receives $value, $key and $default. Runs whenever a Theme Option is read. License keys are read unfiltered.
  • canvas_option_set_{$key} (filter) Receives $value and $key. Runs whenever a Theme Option is written, before it is stored. License keys are written unfiltered.
  • canvas_before_save_options and canvas_after_save_options (actions) Receive $field_id, $value and the field's settings; the after action also receives $result (bool). They fire when one option is saved on its own, as wp canvas option set does. Save Options on the Theme Options screen and an import write every option at once without them, so watch core's update_option_canvas_options action to see every change.

Blocks And Assets

These hooks change which blocks register and what they load:

  • canvas_block_enabled (filter) Receives $enabled (bool), $name and $block. Return false to keep a block from registering. Canvas first reads it while plugins load, so add it from a plugin.
  • canvas_block_enabled_{name} (filter) The same for one block. Receives $enabled and $block. Add it from a plugin as well.
  • canvas_block_classes (filter) Receives the list of block class names Canvas registers. Runs while plugins load, so add it from a plugin.
  • canvas_block_title (filter) Receives $title, $name and $block. The inserter title of a block that does not set its own. Add it from a plugin, because the block list on the Blocks tab of Theme Options is built while plugins load.
  • canvas_block_asset_map (filter) Receives $map (block name => its styles and scripts). Add, remove or replace the stylesheets and scripts a block loads.
  • canvas_responsive_breakpoints (filter) Receives array( 'tablet' => ..., 'mobile' => ... ), CSS max-width values. Moves the breakpoints the responsive block styles compile to, in the editor and on the site alike.
  • canvas_js_options (filter) Receives the cnvsOptions array handed to the theme's front end script.
  • canvas_body_attributes (filter) Receives an array of data-* attribute name => value for the <body> element.
  • canvas_content_width (filter) Receives the embed and image width cap, 1024 by default.
  • canvas_is_dev_env (filter) Receives $is_dev (bool). While true, theme asset URLs carry each file's modification time instead of the version, so a changed file is never served from a stale cache.
  • canvas_register_icon_packs (action) No arguments. Call Canvas_Core\Icons\Registry::register( $name, $args ) here to add an icon pack to the icon picker.
  • canvas_enqueue_icon_packs (action) No arguments. Fires after Canvas enqueues its icon packs on the front end, while icon packs are switched on. Enqueue your pack's stylesheet here with canvas-font-icons as a dependency.
  • canvas_enqueue_assets (action) No arguments. Fires after Canvas enqueues its front end stylesheets and scripts, so you can enqueue yours after them.
  • canvas_theme_setup and canvas_block_theme_setup (actions) No arguments. Fire after Canvas registers its theme supports.
  • canvas_widgets_init (action) No arguments. Fires on widgets_init.
  • canvas_theme_activated (action) No arguments. Fires when the Canvas theme is activated.
  • canvas_core_activated and canvas_core_deactivated (actions) No arguments. Fire when Canvas Core is activated and deactivated.
  • canvas_post_type_is_headless (filter) Receives $headless (bool) and the WP_Post_Type. A headless type has no front end address and no preview link. True by default for a canvas_ type that is not viewable.

Demos

These actions fire around a demo import and its removal:

  • canvas_demo_import_complete (action) Receives $import_id, $demo_slug and $summary (counts). Fires when an import is finalized.
  • canvas_after_demo_import (action) Receives $demo_slug. Fires right after canvas_demo_import_complete.
  • canvas_demo_rollback_complete (action) Receives $import_id and $counts (what was removed). Fires after a rollback.
  • canvas_demo_data_deleted (action) Receives $slug and $summary. Fires after all of a demo's imported data is removed.

The import wizard, wp canvas demo import and the canvas/import-demo ability all run one server side pipeline, so these actions fire whichever of them started the import.

Forms

These hooks run for every Canvas form submission:

  • canvas_form_validate (filter) Receives $errors, $data, $field_defs, $form_id, $settings and $form_key. Add a field name => message pair to refuse the submission before anything is stored, sent or uploaded.
  • canvas_form_submission_data (filter) Receives $data, $form_id, $settings and $meta. The values every action receives.
  • canvas_form_submitted (action) Receives $form_id, $data, $settings, $meta and $results. Fires after the form's actions have run.
  • canvas_form_actions (filter) Receives $actions. The actions a form can run after a submission; add yours to offer it on the form's Actions tab.
  • canvas_form_field_definitions (filter) Receives $fields and $form_id. The field definitions read from the form's blocks.

SEO

These filters apply while Canvas SEO runs, which is whenever no other SEO plugin is active:

  • canvas_seo_title, canvas_seo_description and canvas_seo_canonical (filters) Receive the title, the meta description or the canonical URL (string).
  • canvas_seo_robots (filter) Receives the robots directive string.
  • canvas_seo_schema (filter) Receives the schema graph nodes (array, without the top level @context).
  • canvas_seo_external_active (filter) Receives $active (bool) and $name (the detected plugin, or ''). Return true for an SEO plugin Canvas does not detect, so Canvas SEO steps aside; return false to keep Canvas SEO running beside a detected one. Analytics keeps running either way.

Performance

These filters tune the optimisations on the Canvas > Performance screen, and apply only while the matching option is on:

  • canvas_preconnect_origins and canvas_dns_prefetch_domains (filters) Receive the origins to preconnect to and the domains to prefetch.
  • canvas_fonts_to_preload (filter) Receives stylesheet URLs preloaded with as="style". Preload font files separately.
  • canvas_lcp_preload_sources (filter) Receives the image URLs preloaded for the largest paint.
  • canvas_defer_excluded_scripts (filter) Receives script handles that are never deferred, jquery by default.
  • canvas_async_css_excluded (filter) Receives stylesheet handles that always load normally.
  • canvas_lazy_load_skip_classes (filter) Receives image classes that are never lazy loaded: parallax-bg, no-lazy and skip-lazy by default.
  • canvas_local_fonts_cache_dir and canvas_local_fonts_user_agent (filters) Receive the directory, and the browser identity, Canvas uses for Google Fonts stylesheets it serves from a local copy rather than through the WordPress Font Library, such as a page scoped demo's fonts. The directory defaults to canvas-fonts in uploads.
  • canvas_public_rest_route_prefixes (filter) Receives the REST route prefixes left open to signed out visitors while the REST API is restricted, for example a form plugin's submission route.

WooCommerce

These filters decide how much of WooCommerce's own presentation survives:

  • canvas_use_woocommerce_styles (filter) Receives $use (bool, false). Return true to load WooCommerce's stock stylesheets again.
  • canvas_dequeued_woocommerce_styles (filter) Receives the WooCommerce style handles Canvas removes.
  • canvas_woocommerce_alert_notices (filter) Receives $enabled (bool, true). Return false to keep WooCommerce's own notice markup instead of Canvas alerts.
  • canvas_woocommerce_image_widths_from_options (filter) Receives $from_options (bool, true). Return false to let a theme declared image width stay authoritative.
  • canvas_cart_fragment_applier_handles (filter) Receives the script handles that may apply cart fragments; the mini cart is re-initialised when any of them loads.

Theme Components

These filters adjust individual theme components:

  • canvas_gototop_icon_class (filter) Receives the class list of the go to top button.
  • canvas_gototop_style (filter) Receives the button style, arrow or progress.
  • canvas_comments_show_title and canvas_comments_show_form (filters) Receive true. Return false to hide the comments heading or the comment form.
  • canvas_comment_list_class (filter) Receives the class list of the comment list.
  • canvas_comment_reply_avatar_smaller (filter) Receives false. Return true to shrink a reply's avatar against the comment it answers.
  • canvas_resolve_url_token (filter) Receives $resolved (''), $token and $url. Resolve a {token} URL of your own; returning '' omits the link.

WP-CLI Commands

Canvas Core adds two command groups to WP-CLI. Pass an administrator's login with --user=<login>: the import and every option command check the same capabilities the matching screen checks, and without --user WP-CLI runs as nobody.

How To List The Demos

wp canvas demo list [--format=table|json|csv]

Prints the slug, name and tier of every demo in the catalogue. The slug is what wp canvas demo import expects. Listing needs no --user, because it only reads the catalogue.

How To Import A Demo

wp canvas demo import <slug> [--scope=site|page] --user=<login>

The command runs every step the import wizard runs, in the same order and with the same checks, and prints one line per step. --scope=site, the default, makes the demo the site's design: its Theme Options, site identity, front page and chrome. --scope=page imports its content and leaves the site's settings alone. Plugins the demo requires are installed from WordPress.org and activated, as the wizard does. The run is recorded like a wizard import, so it appears under Canvas > Demo Import, on the Import History tab, where it can be rolled back.

Three checks run before anything is written:

  • The user must be able to manage options. Without --user, WP-CLI runs as nobody and the command stops.
  • The user must be able to publish unfiltered HTML. WordPress filters the content saved by anyone without that capability, and a demo's pages carry markup and attributes the filter would strip, so the command refuses rather than import a damaged design. On a single site that means an Administrator; on a multisite network only a Super Admin has it, so pass a Super Admin's login. A site that defines DISALLOW_UNFILTERED_HTML takes the capability from everyone, so the command cannot import there.
  • The site needs an active license. The import refuses without one, and the demo package is downloaded from the Canvas service only after it has verified the site's license.

For example:

wp canvas demo import agency --user=admin
wp canvas demo import restaurant --scope=page --user=admin

How To Read And Write Theme Options

wp canvas option get <key> [--format=var_export|json|yaml] --user=<login>
wp canvas option set <key> <value> [--format=plaintext|json] --user=<login>
wp canvas option export [--file=<file>] [--format=string|json] --user=<login>
wp canvas option import <file> --user=<login>

set, export and import run the Theme Options screen's own handlers, so a value is sanitised by its setting's type exactly as the screen would, and an export carries exactly what the screen's Export carries. get and export never print a credential or the license, and import skips them.

  • get prints one option. The key must be a registered Theme Option; wp canvas option export --format=json lists them all.
  • set stores one option. Pass --format=json for a list or a group. Setting the value an option already holds reports success and changes nothing. A colour that is not a valid hex colour is refused: the option keeps the colour it had, or takes its default when none was stored.
  • export prints the export string the Theme Options screen's Import accepts, or JSON with --format=json, to the terminal or to --file.
  • import reads either format from a file, or from standard input with -. Only registered options are imported.

The canvas/import-demo Ability

On WordPress 6.9 and later, Canvas registers its abilities with the WordPress Abilities API, in the canvas-demos, canvas-design and canvas-content categories. canvas/import-demo runs the same pipeline as the wizard and the command:

  • Input: slug (string, required, as returned by canvas/list-demos) and scope (site or page, default site).
  • Output: importId, slug, scope and summary (the finalize step's counts).
  • Permission: the calling user must be able to manage options. The license is checked when the import runs, exactly as for the wizard, and a site without an active license gets an error.

The ability is marked destructive and not idempotent, so its REST route answers POST:

POST /wp-json/wp-abilities/v1/abilities/canvas/import-demo/run
Content-Type: application/json

{ "input": { "slug": "agency", "scope": "page" } }

The ability runs every step in one request, where the wizard runs one step per request. On a host with a short request time limit, a large demo is safer imported with wp canvas demo import.

The other abilities are canvas/list-pages and canvas/list-demos (users who can edit pages), canvas/get-page-structure (users who can edit that post), canvas/get-design (users who can edit theme options) and canvas/set-colors and canvas/set-typography (users who can manage options).

Helpers You Can Call

A component template or a plugin can rely on these functions:

  • canvas_render( $component, $props ) renders a component template and returns its markup.
  • canvas_get_option( $key, $default ) reads a Theme Option; canvas_option_is_on( $key, $default ) reads an on or off option in every form it can be stored in.
  • canvas_queue_inline_style( $css ) is the way to add CSS while a component renders. A block theme builds the page before wp_head(), so a <style> element echoed from a template lands in the body; the queue prints the CSS with the head styles instead, or in place where no document follows, such as a block preview. Escape every value first.
  • canvas_queue_inline_script( $js, $handle ) and canvas_queue_script_data( $handle, $object_name, $data ) hand JavaScript and configuration to a script from inside a render, for the same reason.
  • canvas_sanitize_class_string( $classes ) in the theme, and Canvas_Core\Support\ClassList::sanitize( $classes ) in the plugin, sanitise a class list token by token, so bi bi-arrow-right keeps both classes.
  • canvas_resolve_image( $item, $size ) resolves an attachment id, or an item with image_id, image_url and image_alt, to img_url and img_alt.

What A Child Theme Can Override

The Child Theme step of Canvas > Setup Wizard creates a ready child theme, and the same files ship in the theme's inc/child-theme folder: a stylesheet loaded after Canvas's own, on the site and in the editor, an empty theme.json and a functions.php. A file at the same path in the child theme takes precedence over Canvas's:

  • Templates, template parts and patterns, under templates/, parts/ and patterns/, as for any block theme.
  • Component templates, under components/, because Canvas finds them with locate_template().
  • Front end stylesheets, scripts and images, under assets/, because Canvas and Canvas Core look them up with get_theme_file_uri(). A copy of assets/css/blog.css in the child is the one that loads.

A few files always come from Canvas: the PHP under inc/, the admin screens' own assets, and the scripts and stylesheets the theme's front end script loads on demand as a page needs them. Site Editor customisations are stored per theme, which How To Install The Theme explains before you switch.

Removed Hooks

Two actions no longer exist:

  • canvas_footer_widgets
  • canvas_footer_widget_default_N

They belonged to a classic footer component that a block theme never rendered, so a callback attached to them had no effect even before they were removed. The same change removed the classic components/header.php, components/footer.php and components/footer-widgets.php templates and the canvas_render_header(), canvas_render_footer() and canvas_render_footer_widgets() functions. Build the footer's columns with blocks instead: in a Footer record, where the Canvas Footer Widget Area block holds them, or in the Footer template part in the Site Editor. To change a Footer record's markup from code, filter canvas_component_output where $component is footer-builder.

Pitfalls

These are the questions the hooks above raise most often:

  • A field added with canvas_block_fields shows in the sidebar but changes nothing on the page. The value reaches the block's component template as $args['<field id>'], and a template prints only what it reads. Read the value in a child theme copy of the template, or act on it with canvas_component_props as the example does. A block that builds its markup without a component template stores the value but never prints it.
  • A canvas_block_enabled callback in functions.php behaves inconsistently. Canvas decides which blocks exist while plugins load, before any theme code runs, and checks again when it registers them, so a theme callback reaches only the second check. Move the callback into a plugin or a must-use plugin.
  • The page settings panels are missing on a custom post type. The type is not shown in the REST API, does not use the block editor, or does not support custom-fields, so it gets the classic boxes instead. Add custom-fields to its supports.
  • wp canvas option get says to add --user. Theme Options are administrator settings, and WP-CLI runs as nobody by default.
  • wp canvas demo import refuses on a network. Only a Super Admin can publish unfiltered HTML on multisite, and the import needs that capability. Pass a Super Admin's login with --user.
Was this page helpful?
Developer Hook Reference · Canvas WP Docs