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 asicon-box) and$block(the block instance). Runs for every Canvas block.canvas_block_{name}_fields(filter) The same for one block, for examplecanvas_block_icon-box_fields. Receives$fieldsand$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}.phpthentemplate-parts/{name}.php) and$component. The list is resolved withlocate_template(), so a child theme's copy wins.canvas_component_props(filter) Receives$propsand$component. Change the values a template receives, as the example above does.canvas_component_output(filter) Receives$output(string),$componentand$props. Change the rendered markup.canvas_before_renderandcanvas_after_render(actions) Receive$componentand$props; the after action also receives$output.
Header, Footer And Page Chrome
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 ofdata-*attribute name => value) and$config. The attributes the header script reads, such asdata-sticky-classanddata-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 oncanvas_after_header, so a transparent header keeps its transparent scheme.canvas_header_suppressed(filter) Receives$suppressed(bool),$configand$post_id. Return true to emit no header at all. Content oncanvas_after_headerstill renders.canvas_top_bar_suppressed(filter) Receives$suppressedand$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-menuby default). Top level blocks of a built header with these names are printed after</header>instead of inside it.canvas_before_headerandcanvas_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 oncanvas_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_contentandcanvas_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_contentandcanvas_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_textandcanvas_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) Receivesnulland$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_typesThe 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_typesandcanvas_page_layout_post_typesThe final list for each of those three panels.canvas_page_title_post_typesPage Title Settings. Defaults to every public post type with an admin screen, except attachments.canvas_slider_meta_post_typesSlider. 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_typesandcanvas_side_panel_meta_post_typesHero, Footer, Copyrights and Side Panel. Default to pages and posts.canvas_seo_meta_post_typesCanvas 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_sectionsandcanvas_register_option_fields(actions) Receive theCanvas_Core\Options\Fieldsinstance. Call itsregister_section( $id, $settings )andregister_field( $id, $settings )to add your own Theme Options tab and settings.canvas_option_{$key}(filter) Receives$value,$keyand$default. Runs whenever a Theme Option is read. License keys are read unfiltered.canvas_option_set_{$key}(filter) Receives$valueand$key. Runs whenever a Theme Option is written, before it is stored. License keys are written unfiltered.canvas_before_save_optionsandcanvas_after_save_options(actions) Receive$field_id,$valueand the field's settings; the after action also receives$result(bool). They fire when one option is saved on its own, aswp canvas option setdoes. Save Options on the Theme Options screen and an import write every option at once without them, so watch core'supdate_option_canvas_optionsaction to see every change.
Blocks And Assets
These hooks change which blocks register and what they load:
canvas_block_enabled(filter) Receives$enabled(bool),$nameand$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$enabledand$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,$nameand$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 => itsstylesandscripts). Add, remove or replace the stylesheets and scripts a block loads.canvas_responsive_breakpoints(filter) Receivesarray( '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 thecnvsOptionsarray handed to the theme's front end script.canvas_body_attributes(filter) Receives an array ofdata-*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. CallCanvas_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 withcanvas-font-iconsas 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_setupandcanvas_block_theme_setup(actions) No arguments. Fire after Canvas registers its theme supports.canvas_widgets_init(action) No arguments. Fires onwidgets_init.canvas_theme_activated(action) No arguments. Fires when the Canvas theme is activated.canvas_core_activatedandcanvas_core_deactivated(actions) No arguments. Fire when Canvas Core is activated and deactivated.canvas_post_type_is_headless(filter) Receives$headless(bool) and theWP_Post_Type. A headless type has no front end address and no preview link. True by default for acanvas_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_slugand$summary(counts). Fires when an import is finalized.canvas_after_demo_import(action) Receives$demo_slug. Fires right aftercanvas_demo_import_complete.canvas_demo_rollback_complete(action) Receives$import_idand$counts(what was removed). Fires after a rollback.canvas_demo_data_deleted(action) Receives$slugand$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,$settingsand$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,$settingsand$meta. The values every action receives.canvas_form_submitted(action) Receives$form_id,$data,$settings,$metaand$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$fieldsand$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_descriptionandcanvas_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_originsandcanvas_dns_prefetch_domains(filters) Receive the origins to preconnect to and the domains to prefetch.canvas_fonts_to_preload(filter) Receives stylesheet URLs preloaded withas="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,jqueryby 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-lazyandskip-lazyby default.canvas_local_fonts_cache_dirandcanvas_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 tocanvas-fontsin 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,arroworprogress.canvas_comments_show_titleandcanvas_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(''),$tokenand$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_HTMLtakes 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=adminHow 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=jsonlists them all. - set stores one option. Pass
--format=jsonfor 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 bycanvas/list-demos) andscope(siteorpage, defaultsite). - Output:
importId,slug,scopeandsummary(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 beforewp_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 )andcanvas_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, andCanvas_Core\Support\ClassList::sanitize( $classes )in the plugin, sanitise a class list token by token, sobi bi-arrow-rightkeeps both classes.canvas_resolve_image( $item, $size )resolves an attachment id, or an item withimage_id,image_urlandimage_alt, toimg_urlandimg_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/andpatterns/, as for any block theme. - Component templates, under
components/, because Canvas finds them withlocate_template(). - Front end stylesheets, scripts and images, under
assets/, because Canvas and Canvas Core look them up withget_theme_file_uri(). A copy ofassets/css/blog.cssin 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_widgetscanvas_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_fieldsshows 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 withcanvas_component_propsas the example does. A block that builds its markup without a component template stores the value but never prints it. - A
canvas_block_enabledcallback infunctions.phpbehaves 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. Addcustom-fieldsto itssupports. wp canvas option getsays to add--user. Theme Options are administrator settings, and WP-CLI runs as nobody by default.wp canvas demo importrefuses 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.
What To Read Next
- How To Install The Theme, for installing the child theme and the Site Editor caveat
- How To Import A Demo, the wizard the command and the ability share their pipeline with
- Shared Block Controls, the panels every block carries before you add your own fields
