Skip to content
← Back to Blog

Build a Custom Gutenberg CTA Block in TailPress Without ACF

A practical guide to building native Gutenberg blocks inside a TailPress theme without ACF. Learn how to create a custom CTA block using block.json, WordPress editor components, and Tailwind CSS for a cleaner, lightweight WordPress workflow.

Build a Custom Gutenberg CTA Block in TailPress Without ACF
Build a Custom Gutenberg CTA Block in TailPress Without ACF

If you are building WordPress themes with TailPress, there is a good chance you already enjoy writing your layouts with Tailwind CSS. But when it comes to custom Gutenberg blocks, many developers immediately reach for ACF Pro.

ACF is excellent, especially for complex fields and client-friendly content editing. But for simple layout blocks like call-to-action sections, hero banners, feature boxes, testimonials, and alerts, you do not always need an extra plugin.

In this guide, we will build a native Gutenberg CTA block inside a TailPress theme using block.json, JavaScript, React-powered Gutenberg components, and Tailwind CSS.

No ACF. No heavy setup. Just a clean theme-based block workflow.

What We Are Building

We will create a custom CTA block that lets editors update:

  • Heading text
  • Short description
  • Button label
  • Button URL

The final block will be available directly inside the Gutenberg editor and will render Tailwind-styled HTML on the frontend.

Why Build Native Blocks?

Native Gutenberg blocks are useful when you want tighter control over your theme and fewer dependencies.

This approach works well when:

  1. Your block is mostly presentational
  2. You already use Tailwind CSS in your theme
  3. You want blocks stored in your codebase
  4. You want fewer plugin dependencies
  5. You want a faster, cleaner deployment flow

For complex repeaters, relationship fields, and advanced admin workflows, ACF still makes sense. But for clean design sections, native blocks can be a better fit.

How to Create Custom Gutenberg Blocks Without ACF

You can create custom Gutenberg blocks without ACF by registering a

native WordPress block with `block.json`, building the editor interface

with Gutenberg's JavaScript components, and registering the block in

your theme or plugin.

In a TailPress project, Tailwind CSS can then be used for both the

editor and frontend styling. This keeps the block inside your codebase

and removes the need for ACF Pro for simple content and layout blocks.

In this tutorial, we'll use that native approach to build a reusable

CTA block step by step.

Step 1: Create a Blocks Folder

Inside your TailPress theme, create a folder for custom blocks:

BASH
mkdir -p resources/blocks/cta

Your structure will look like this:

BASH
resources/  blocks/    cta/      block.json      index.js      edit.js      save.js

You can create more blocks later using the same pattern.

Step 2: Add the Block Metadata

Create this file:

BASH
resources/blocks/cta/block.json

Add the following:

BASH
{  "apiVersion": 3,  "name": "webixyhub/cta",  "title": "CTA Section",  "category": "design",  "icon": "megaphone",  "description": "A simple call-to-action section with heading, text, and button.",  "keywords": ["cta", "button", "section"],  "supports": {    "align": ["wide", "full"],    "anchor": true  },  "attributes": {    "heading": {      "type": "string",      "source": "html",      "selector": "h2"    },    "text": {      "type": "string",      "source": "html",      "selector": "p"    },    "buttonText": {      "type": "string",      "default": "Get Started"    },    "buttonUrl": {      "type": "string",      "default": "#"    }  },  "editorScript": "webixyhub-cta-block"}

The important part here is the name and editorScript.

The block name must be unique. A good format is:

BASH
namespace/block-name

For example:

BASH
webixyhub/cta

Step 3: Register Blocks in PHP

Now open your theme’s functions.php file and register the block.

BASH
function webixyhub_register_theme_blocks() {    $block_dir = get_stylesheet_directory() . '/resources/blocks/cta';    $script_path = get_stylesheet_directory() . '/blocks/cta.js';    if ( ! file_exists( $block_dir . '/block.json' ) || ! file_exists( $script_path ) ) {        return;    }    wp_register_script(        'webixyhub-cta-block',        get_stylesheet_directory_uri() . '/blocks/cta.js',        array(            'wp-blocks',            'wp-element',            'wp-i18n',            'wp-block-editor',            'wp-components'        ),        filemtime( $script_path ),        true    );    register_block_type( $block_dir );}add_action( 'init', 'webixyhub_register_theme_blocks' );

This tells WordPress to read your block.json file and load the compiled JavaScript file for the editor.

Step 4: Create the Edit Component

Create:

BASH
resources/blocks/cta/edit.js

Add this code:

BASH
import { __ } from '@wordpress/i18n';import {  useBlockProps,  RichText,  URLInputButton} from '@wordpress/block-editor';import { TextControl } from '@wordpress/components';export default function Edit({ attributes, setAttributes }) {  const { heading, text, buttonText, buttonUrl } = attributes;  const blockProps = useBlockProps({    className:      'bg-slate-950 text-white px-6 py-14 rounded-lg text-center'  });  return (    <section {...blockProps}>      <div className="mx-auto max-w-3xl">        <RichText          tagName="h2"          className="text-3xl font-bold"          placeholder={__('Add CTA heading...', 'webixyhub')}          value={heading}          onChange={(value) => setAttributes({ heading: value })}        />        <RichText          tagName="p"          className="mt-4 text-base text-slate-300"          placeholder={__('Add short description...', 'webixyhub')}          value={text}          onChange={(value) => setAttributes({ text: value })}        />        <div className="mt-6 flex flex-col items-center gap-3">          <TextControl            label={__('Button Text', 'webixyhub')}            value={buttonText}            onChange={(value) => setAttributes({ buttonText: value })}          />          <URLInputButton            url={buttonUrl}            onChange={(value) => setAttributes({ buttonUrl: value })}          />        </div>        <div className="mt-6 inline-flex rounded-md bg-white px-5 py-3 text-sm font-semibold text-slate-950">          {buttonText || __('Button Text', 'webixyhub')}        </div>      </div>    </section>  );}

This is what the editor will display inside Gutenberg.

We are using:

  • useBlockProps() for proper block wrapper behavior
  • RichText for editable heading and paragraph content
  • TextControl for button label
  • URLInputButton for button link

Step 5: Create the Save Component

Create:

BASH
resources/blocks/cta/save.js

Add this:

BASH
import { useBlockProps, RichText } from '@wordpress/block-editor';export default function Save({ attributes }) {  const { heading, text, buttonText, buttonUrl } = attributes;  const blockProps = useBlockProps.save({    className:      'bg-slate-950 text-white px-6 py-14 rounded-lg text-center'  });  return (    <section {...blockProps}>      <div className="mx-auto max-w-3xl">        <RichText.Content          tagName="h2"          className="text-3xl font-bold"          value={heading}        />        <RichText.Content          tagName="p"          className="mt-4 text-base text-slate-300"          value={text}        />        {buttonText && (          <a            href={buttonUrl || '#'}            className="mt-6 inline-flex rounded-md bg-white px-5 py-3 text-sm font-semibold text-slate-950 hover:bg-slate-200"          >            {buttonText}          </a>        )}      </div>    </section>  );}

The save file controls the HTML that gets stored and rendered on the frontend.

Step 6: Register the Block in JavaScript

Create:

BASH
resources/blocks/cta/index.js

Add:

BASH
import { registerBlockType } from '@wordpress/blocks';import Edit from './edit';import Save from './save';registerBlockType('webixyhub/cta', {  edit: Edit,  save: Save});

Make sure the block name here matches the name inside block.json.

Step 7: Compile the Block JavaScript

If your TailPress setup uses Laravel Mix, add a separate build entry in webpack.mix.js:

BASH
mix.js('resources/blocks/cta/index.js', 'blocks/cta.js');

If you want to auto-compile multiple blocks later, you can set up a glob-based workflow, but for one block this is enough and easier to understand.

Now run:

BASH
npm run watch

Or for production:

BASH
npm run production

After the build, you should have:

BASH
blocks/cta.js

Step 8: Add Tailwind Content Paths

Make sure Tailwind scans your block files.

In tailwind.config.js, include:

BASH
content: [  './**/*.php',  './resources/**/*.js',  './resources/blocks/**/*.{js,jsx}']

Without this, Tailwind may remove the classes used inside your block files during production builds.

Step 9: Test in WordPress

Now go to your WordPress admin:

  • Open any page or post.
  • Click the block inserter.
  • Search for “CTA Section”.
  • Add the block.
  • Edit the heading, text, and button.
  • Publish the page.

If everything is connected correctly, your custom CTA block should appear in the editor and render on the frontend with Tailwind styling.

Common Issues

Block Is Not Showing

Check these things:

  • Your theme is active
  • block.json exists
  • The block name matches in block.json and index.js
  • The script handle matches editorScript
  • The compiled file exists in blocks/cta.js

Tailwind Styles Are Missing

Usually this means Tailwind is not scanning your block files.

Check your tailwind.config.js content paths and rebuild your CSS.

JavaScript Error in Editor

Open the browser console and check the exact error. Most issues come from a wrong import, missing dependency, or mismatch between the registered block name and metadata.

When Should You Still Use ACF?

Use ACF when your block needs:

  • Repeaters
  • Complex conditional fields
  • Relationship fields
  • Client-friendly admin field controls
  • Dynamic PHP-heavy rendering

But if your block is mostly layout and content, native Gutenberg blocks can keep your theme lighter and more maintainable.

Final Thoughts

Building custom Gutenberg blocks inside TailPress without ACF gives you a cleaner WordPress development workflow. Your blocks stay inside the theme, your styling stays in Tailwind, and your editor experience feels native.

For simple sections like CTAs, heroes, feature grids, pricing boxes, and testimonials, this approach is fast, flexible, and easy to maintain.

Once you understand this pattern, you can reuse it for almost any design block in your WordPress theme.

More Articles

Laravel How to Build an AI-Powered Laravel App with Laravel AI SDK Laravel How to Integrate Google reCAPTCHA v2 in a Laravel Contact Form