With version 5.0, WordPress introduced a new post editing experience called Gutenberg. Instead of a rich text writing flow, the new editor prioritizes blocks to design and create content.

Creating custom blocks is not a trivial task, as Javascript is now needed on all steps to do so. As of now, there is no native way to create custom blocks using PHP, the programming language that WordPress is built on.

Advanced Custom Fields, WordPress' most popular custom field solution, launched their own wrapper to create custom blocks using PHP with version 5.8. Here at Fabrikat, we were so blown away by the possibilities of this solution that we decided to build an internal framework that makes composing ACF blocks easier and quicker. The result is Blockstudio. (BS)

Under the hood

BS is making heavy use of the acf_register_block_type function. This means that every block made with BS is an ACF block under the hood, exposed to the same block attributes and settings that are available to ACF blocks not made with BS.


Recent discussions on WordPress vs Jamstack show that developers are increasingly favouring new ways of working. Components have made a big impact on the way sites and apps are built in the current day, with React, Vue and Angular revolutionising a jQuery past.

While the bare functionality of BS is not forcing any of those methodologies on it's users, the accompanying library is trying to find a middle ground between the old and the new and is thus making use of one framework: Alpine JS.

It's declarative and reactive nature brings the benefits of the libraries mentioned above to any DOM and is thus perfect for a new WordPress world consisting of blocks.

Why ACF blocks

You might wonder at this stage, why bother with Advanced Custom Fields for block creation when you could just use WordPress core? While we have been toying with React based blocks for a bit, constructing new elements with ACF has one big advantage over the "WordPress way":

Dynamic blocks don't need to duplicate the rendering logic in Javascript and PHP for each block. This is huge, especially when the list of blocks is constantly growing. There is a healthy discussion on this topic in the Gutenberg GitHub if you want to learn more about the issue.

Since the single file component approach is so central to BS, using ACF blocks is the best option out there right now. Of course, we will keep our ears open to new developments and extend BS to use new APIs where we see fit!


The blocks creation process is heavily inspired by the work of Maciej Palmowski and his Timber WP blocks plugin.

Creating blocks

Composing your own blocks with BS is extremely easy. The plugin will look for a blockstudio folder within your currently activated theme. Inside of it, every .php file will create a new block by making use of the file header information. Let's take a look at one of the blocks that is being used on the landing page:

<?php // cta.php /* Title: Call to Action Description: Call to action element. Category: blockstudio Icon: lightbulb Mode: preview SupportsAlign: false SupportsMode: false */

The above is the equivalent of this code:

add_action( 'acf/init', 'my_register_blocks' ); function register_cta_block() { if( function_exists( 'acf_register_block_type' ) ) { register_cta_block(array( 'name' => 'cta', 'title' => __('Call to Action'), 'description' => __('Call to action element.'), 'category' => 'blockstudio', 'icon' => 'lightbulb', 'mode' => 'preview', 'supports' => array( 'align' => false, 'mode' => false, ), )); } }

While both of these examples will register a block, nothing is being rendered yet. Instead of relying on callbacks or templates, markup in BS is being defined in the same file like so:

<?php // cta.php /* Title: Call to Action Description: Call to action element. Category: blockstudio Icon: lightbulb Mode: preview SupportsMode: false SupportsAlign: false */ ?> <p>Buy my product!</p>

And that's it, we made our first block! Just like ACF itself, every filename can only contain lowercase alphanumeric characters and dashes, and must begin with a letter. We are working hard to support all parameters that ACF is providing, at the moment following settings are allowed/working:

<?php /* Title: Testimonial Description: Content block for customer testimonials. Category: marketing Icon: smiley Keywords: testimonial customer content Mode: preview Align: full PostTypes: page SupportsMode: false SupportsAlign: false SupportsMultiple: false SupportsJSX: false */ ?>

If you need to access more advanced features with PHP, it is also possible to register blocks using the $register variable:

<?php $register = array( 'title' => 'First Block', 'title' => __( 'First Block' ), 'category' => 'formatting', 'icon' => 'design', ); ?> <p>My Markup</p>

Alternatively, it is possible to add settings via PHP while maintaining settings set with the file header using the $settings variable:

<?php // cta.php /* Title: Call to Action Description: Call to action element. Category: blockstudio Icon: lightbulb Mode: preview SupportsMode: false SupportsAlign: false */ $settings = array( 'text-align' => 'left' ); ?> <p>Buy my product!</p>


Since BS is built on ACF, every block will automatically be registered under the acf namespace. In our example above, the block name would have been acf/cta.

It is also possible to create blocks using folders within the Blockstudio folder. To avoid duplicate block names and errors, every block within a subfolder will be prefixed with the folders name. For example, a file called testimonial.php inside a marketing subfolder will have acf/marketing-testimonial as it's block name.

Unlimited nested folders are supported.


It may make sense to prefix all blocks if you already have an existing ACF blocks library. The blockstudio/prefix filter does exactly that:

<?php add_filter( 'blockstudio/prefix', function () { return 'bs'; } );

The code above would convert the block name from acf/my-block to acf/bsx-my-block. The filter is used internally for the block library with a blockstudio prefix, so avoid this particular prefix.

Custom paths

Changing the location of the folder that is going to be searched for blocks might be necessary, for example if you are using a page builder like Oxygen, which disables the theme altogether. Use the blockstudio/path filter to do so:

<?php // Custom path within your theme. add_filter( 'blockstudio/path' , function () { return get_template_directory() . '/blocks'; } ); // Custom path within a plugin. add_filter( 'blockstudio/path' , function () { return plugins_url . '/my-custom-plugin/blocks'; } );

Multiple instances

If the above options are not enough, it is possible to initiate the Blockstudio\Build class on various folders of your choice:

<?php add_action( 'acf/init', function () { // Blockstudio\Build::init( $folder, $prefix ); Blockstudio\Build::init( 'client-blocks', 'client' ); Blockstudio\Build::init( 'documentation-blocks', 'documentation' ); } );

Using different prefixes is highly advisable in this case, as it'll make sure that no two block names will be the same.

Defining fields

Of course, the real usefulness of blocks come from connecting custom fields to them. This way, blocks can be infinitely reused with different settings or texts.

While defining fields via the ACF interface is completely fine, BS includes a handy way to register fields in the block files like so:

<?php // cta.php /* Title: Call to Action Description: Call to action element. Category: design Icon: lightbulb Mode: preview SupportsAlign: false SupportsMode: false */ $fields = array( array( 'key' => 'cta_title_key', 'label' => 'Title', 'name' => 'cta_title', 'type' => 'text', ), array( 'key' => 'cta_text_key', 'label' => 'Text', 'name' => 'cta_text', 'type' => 'textarea', ), ); ?> <h1><?php echo get_field( 'cta_title' ) ?: 'Title'; ?></h1> <p><?php echo get_field( 'cta_text' ) ?: 'Text'; ?></p>

Blockstudio is automatically going to look for a $fields variable inside your files and if available, will register those and connect them to the block. All of the settings that ACF offers when registering fields via PHP are available here.


Previews make it possible to show the block in the inserter panel on hover. This feature behaves similar to the field registration and has to be defined in an $example variable inside your block file. All field data is called with $block['data'] or get_field() is available. Below is an example of the heading block used on the landing page.

<?php /* Title: Heading Description: Simple heading. Category: blockstudio Icon: editor-paragraph Mode: preview SupportsAlign: false SupportsMode: false */ $fields = array( array( 'key' => 'heading_heading_key', 'label' => 'Heading', 'name' => 'heading_heading', 'type' => 'text', ), array( 'key' => 'heading_text_key', 'label' => 'Text', 'name' => 'heading_text', 'type' => 'text', ), ); $example = array( 'attributes' => array( 'mode' => 'preview', 'data' => array( 'heading_heading' => "Title", 'heading_text' => "Text" ) ) ); $heading = get_field( 'heading_heading' ); $text = get_field( 'heading_text' ); ?> <div class="<?php echo $block['blockstudio_center']; ?>"> <?php if ( $heading ): ?> <h1 class="<?php echo $block['blockstudio_text_2xl']; ?>"> <?php echo $heading; ?> </h1> <?php endif; ?> <?php if ( $text ): ?> <p class="<?php echo $block['blockstudio_text_md']; ?> bsx-text-gray-600 2xl:bsx-max-w-none inter"> <?php echo $text; ?> </p> <?php endif; ?> </div>


It is possible to set global defaults for all blocks registered with Blockstudio using the blockstudio/defaults filter:

<?php add_filter( 'blockstudio/defaults', function () { return array( 'post_types' => array( 'post' ), ); } );

The above code will limit all BS blocks to posts. Setting the post type on individual blocks will override the global setting.

Generating classes

The classFilter is an essential part of Blockstudio, since it is able to automatically generate cohesive CSS classes for your blocks.

<div class="<?php echo Blockstudio\Library::classFilter( $block, $name, $type, $modifier, $post_id ) ?>">Hello</div> <!-- Follows BEM naming structure. <!-- Block scoped and global class without prefix. --> <div class="block-name__element-name--modifier"/> <div class="block-name__element-name--modifier"/> <!-- Block scoped and global class with prefix. --> <div class="prefix-block-name__element-name--modifier"/> <div class="prefix-block-name__element-name--modifier"/>

The function accepts following parameters:

  • $block: this is the ACF block array and required as it will use the block prefix and name for the generation of the class.
  • $name: the main name for the class and required as well. It makes sense to pick a descriptive term like 'container' or 'h1' here. This parameter can also be an array and generate multiple classes.
  • $type: string to indicate what kind of classes should be generated. choice between 'all', 'global' or 'block'. (optional, defaults to 'all')
  • $modifiers: an array of strings that will be added as a modifier to the end of the class name. (optional, one class per array entry)
  • $post_id: passing this will add the post ID to the filter, making it possible to reference it in your filter actions. (optional)

Let's illustrate the above with a simple hero block:

<?php // functions.php add_filter( 'blockstudio/prefix', function () { return 'twenty'; } ); // hero.php /* Title: Hero Description: Hero block. Category: design Icon: cover-image Mode: preview SupportsAlign: false SupportsMode: false */ $fields = array( array( 'key' => 'layout_field', 'name' => 'layout', 'type' => 'checkbox', 'choices' => array( 'full-height' => 'full-height', 'full-width' => 'full-width', ), ) ); $modifier = get_field( 'layout' ); ?> <div class="<?php echo Blockstudio\Library::classFilter( $block, 'wrapper', 'block', $modifier ) ?>"> <div class="<?php echo Blockstudio\Library::classFilter( $block, 'text' ) ?>"> <h1 class="<?php echo Blockstudio\Library::classFilter( $block, 'h1' ) ?>">My hero headline</h1> <p class="<?php echo Blockstudio\Library::classFilter( $block, 'p' ) ?>">My hero text</p> </div> </div>

With the full-width checkbox checked in the editor, the following markup would be generated on the frontend:

<div class="twenty-hero__wrapper--full-width twenty-hero__wrapper"> <div class="twenty__text twenty-hero__text"> <h1 class="twenty__h1 twenty-hero__h1">My hero headline</h1> <p class="twenty__p twenty-hero__p">My hero text</p> </div> </div>

Adding classes

While the CSS class generation might be the perfect solution for some, others might rather apply utility classes to all elements marked with the classFilter. This is done by hooking into the acf/register_block_type_args filter, which enables the addition of new data to the blocks.

Let's add some classes to the hero element from before:

<?php add_filter( 'acf/register_block_type_args', function ( $args ) { // Only add arguments for blocks created with Blockstudio. $blocks = array( 'acf/twenty-hero', ); if ( in_array( $args['name'], $blocks ) ) { // List of classes. $args['blockstudio/user/twenty/class/block/hero/wrapper'] = 'px-8 py-24 mx-auto max-w-screen-sm lg:px-16 lg:max-w-screen-2xl'; $args['blockstudio/user/twenty/class/text'] = 'break-word space-y-6 md:space-y-8'; $args['blockstudio/user/twenty/class/h1'] = 'text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl'; $args['blockstudio/user/twenty/class/p'] = 'text-sm leading-relaxed md:text-base lg:text-lg lg:leading-relaxed'; return $args; } } );

The classes will be applied automatically to the specific blocks or to all elements with the same filter name.

<div class="twenty-hero__wrapper--full-width twenty-hero__wrapper px-8 py-24 mx-auto max-w-screen-sm lg:px-16 lg:max-w-screen-2xl"> <div class="twenty__text twenty-hero__text break-word space-y-6 md:space-y-8"> <h1 class="twenty__h1 twenty-hero__h1 text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl">My hero headline</h1> <p class="twenty__p twenty-hero__p text-sm leading-relaxed md:text-base lg:text-lg lg:leading-relaxed">My hero text</p> </div> </div> <?php // Creates global and block classes. Blockstudio\Library::classFilter( $block, 'h1' ); // Works. $args['blockstudio/user/twenty/class/h1'] = 'text-4xl font-semibold tracking-tight md:text-5xl lg:text-6xl'; $args['blockstudio/user/twenty/class/block/hero/h1'] = 'text-primary-600'; // Creates global classes. Blockstudio\Library::classFilter( $block, 'wrapper', 'global' ); // Works. $args['blockstudio/user/twenty/class/wrapper'] = 'bg-primary-50'; // Won't work. $args['blockstudio/user/twenty/class/block/hero/wrapper'] = 'px-8 py-24 mx-auto max-w-screen-sm lg:px-16 lg:max-w-screen-2xl'; // Creates block type specific classes. Blockstudio\Library::classFilter( $block, 'wrapper', 'block' ); // Works. $args['blockstudio/user/twenty/class/block/hero/wrapper'] = 'px-8 py-24 mx-auto max-w-screen-sm lg:px-16 lg:max-w-screen-2xl'; // Won't work. $args['blockstudio/user/twenty/class/wrapper'] = 'bg-primary-50'; // Creates ID specific classes. Blockstudio\Library::classFilter( $block, 'wrapper' ); $args['blockstudio/user/twenty/class/id/block_5fad1acc5c9c4/wrapper'] = 'h-screen'; // Creates modifier classes. Blockstudio\Library::classFilter( $block, 'wrapper', 'block', ['full-screen'] ); $args['blockstudio/user/twenty/class/block/hero/wrapper/full-screen'] = 'bg-primary-600';

Filter classes

As the name of the function suggests, custom filters are created for every instance of the function. Here, the names correlate to it's $args counterpart from before. Let's apply a h-screen class to the wrapper element of our previous hero block:

<?php add_filter( 'blockstudio/user/twenty/class/block/hero/wrapper', function ( $class ) { return $class . ' h-screen'; } );

The filter accepts a $class parameter, which represents the current classes. Three different filters are generated when using the function:

<?php // Global filter - will add the class to all instances where 'wrapper' name was used. // Keep in mind that this won't work when the $type parameter is false. add_filter( 'blockstudio/user/twenty/wrapper', function ( $class ) { return $class . ' h-screen'; } ); // Block filter - will only add the class to a specific block. add_filter( 'blockstudio/user/twenty/class/block/hero/wrapper', function ( $class ) { return $class . ' w-full'; } ); // ID filter - will add the class to a specific block id. add_filter( 'blockstudio/user/twenty/class/id/block_5fad1acc5c9c4/wrapper', function ( $class ) { return $class . ' px-20'; } ); // If the $modifier parameter is set, additional filters are generated depending on the modifier names. add_filter( 'blockstudio/user/twenty/class/block/hero/wrapper/full-width', function ( $class ) { return $class . ' h-screen'; } );

Stay in the loop


You can unsubscribe at any time by clicking the link in the footer of our emails. For information about our privacy practices, please visit our website.

We use Mailchimp as our marketing platform. By clicking to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing.

Learn more about Mailchimp's privacy practices here.


A collection of web components
for WordPress developers.

A modern component-like
ACF blocks framework.

A minimal and feature-packed
WordPress theme.


Documentation Privacy Login Reset password

© 2021 Fabrikat. All rights reserved.