Creating a Custom Block
Build a new Styble block end to end — the JavaScript editor side and the PHP registration/render side.
A Styble block has two halves that share the same slug: a JavaScript folder under src/blocks/ (the editor experience) and a PHP folder under blocks/Types/ (registration and optional server-side rendering). This guide walks through both. If you just want to scaffold the files, jump to the skill.
See the Architecture overview first for the big picture.
The /add-new-block skill
The fastest path is the project's custom skill, which scaffolds both sides following project conventions:
/add-new-block blockName="my-custom-block" blockTitle="My Custom Block" blockDescription="A description" isDynamic=false| Parameter | Required | Notes |
|---|---|---|
blockName | yes | kebab-case slug, e.g. my-custom-block |
blockTitle | yes | Human-readable title |
blockDescription | no | Block description |
isDynamic | no | true for PHP-rendered blocks, false for static |
parentBlock | no | Parent block slug for nested/child blocks |
category | no | Defaults to styble |
It creates the JS files (index.js, edit.jsx, save.jsx, inspect.js, dynamicCss.js, editor.scss, style.scss), the PHP files (<Block_Name>.php, attributes.php), and registers the block in src/blocks/index.js. The rest of this page explains what those files do so you can edit them confidently.
JavaScript side (src/blocks/<block-name>/)
index.js — register the block
Styble blocks register through a thin wrapper, registerStybleBlock (aliased as @styble-pro/registerStybleBlock), which calls registerBlockType() under the hood and applies shared defaults.
import { registerStybleBlock } from '@styble-pro/registerStybleBlock';
import { __ } from '@wordpress/i18n';
import Edit from './edit.jsx';
import save from './save.jsx';
import { MyBlockIcon } from '@styble-pro/blocksIcons';
const options = {
apiVersion: 3,
category: 'styble',
name: 'styble/my-custom-block',
title: __( 'My Custom Block', 'styble-pro' ),
description: __( 'A description.', 'styble-pro' ),
icon: <MyBlockIcon />,
supports: {
align: [ 'wide', 'full' ],
},
edit: ( props ) => <Edit { ...props } />,
save,
};
registerStybleBlock( options.name, options );edit.jsx — the editor UI
edit.jsx renders the block inside the editor and wires up inspector controls. Use useBlockProps() for the wrapper and pull in shared controls from @styble-pro/controls.
import { useBlockProps } from '@wordpress/block-editor';
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
return <div { ...blockProps }>{ /* editor markup + inspector */ }</div>;
}For dynamic blocks, the editor typically previews data fetched through the useApiData hook or @wordpress/server-side-render.
save.jsx — the saved markup
- Static block: return the markup to persist in post content.
- Dynamic block: return
nullso WordPress calls the PHPrender_block()instead.
import { useBlockProps } from '@wordpress/block-editor';
// Static block:
export default function save( { attributes } ) {
const blockProps = useBlockProps.save();
return <div { ...blockProps }>{ /* saved markup */ }</div>;
}
// Dynamic block:
// export default function save() { return null; }Register in src/blocks/index.js
Every block folder is imported in src/blocks/index.js so it loads in the editor bundle. Add a line alongside the others:
import './my-custom-block';Inspector controls
Add settings panels with the project's control components and the Inspector Control skill (add-inspector-control). Controls and constants live under src/controls/ (@styble-pro/controls, @styble-pro/constants).
PHP side (blocks/Types/<Block_Name>/)
The folder name is StudlyCase with underscores — My_Custom_Block for the slug my-custom-block. Blocks_Register scans this directory and instantiates the matching class.
The block class
Create blocks/Types/My_Custom_Block/My_Custom_Block.php extending AbstractBlock:
<?php
namespace ShapedPlugin\StyblePro\Types\My_Custom_Block;
use ShapedPlugin\StyblePro\Includes\AbstractBlock;
defined( 'ABSPATH' ) || exit;
class My_Custom_Block extends AbstractBlock {
/**
* Set block-specific properties.
*/
protected function set_block_properties() {
$this->block_name = 'my-custom-block';
$this->is_dynamic = true; // false for static blocks
// Optionally: $this->scripts, $this->styles, $this->keywords,
// $this->skip_common_attributes (true for child blocks).
}
/**
* Render the block (dynamic blocks only).
*
* @param array $attributes Block attributes.
* @param string $content Inner content.
* @param \WP_Block|null $block Block instance.
* @return string
*/
public function render_block( $attributes, $content = '', $block = null ) {
return '<div>' . esc_html( $attributes['someText'] ?? '' ) . '</div>';
}
}How it works:
AbstractBlock::__construct()callsset_block_properties(), thenload_attributes(), thenregister_block().register_block()builds the args viaget_args()and callsregister_block_type( 'styble/my-custom-block', $args ).api_versionis3and the category isstyble.- For dynamic blocks (
is_dynamic = true),get_args()setsrender_callbacktorender_block_callback(), which firesstyble_pro_before_render/styble_pro_after_renderactions around yourrender_block(). See Hooks.
Block name matters
block_name (kebab-case) must match the src/blocks/<block-name>/ folder and the styble/<block-name> name you registered in JS. The PHP folder Types/My_Custom_Block/ is its StudlyCase form.
attributes.php
Block-specific attributes live in blocks/Types/My_Custom_Block/attributes.php, returning an array. AbstractBlock::load_attributes() merges these on top of CommonAttributes::get() (unless skip_common_attributes is set). Full details in the Attributes guide.
<?php
defined( 'ABSPATH' ) || exit;
return array(
'someText' => array(
'type' => 'string',
'default' => '',
),
);Style class (blocks/Includes/Styles/)
For server-rendered or dynamically styled blocks, add a style class such as blocks/Includes/Styles/MyCustomBlock.php and wire it into DynamicCssGenerator. This produces the per-block CSS from attributes. See Dynamic CSS Generation.
Checklist
- [ ]
src/blocks/<block-name>/withindex.js,edit.jsx,save.jsx - [ ] Import added to
src/blocks/index.js - [ ]
blocks/Types/<Block_Name>/<Block_Name>.phpextendingAbstractBlock - [ ]
set_block_properties()setsblock_name(andis_dynamic) - [ ]
render_block()implemented for dynamic blocks (andsave.jsxreturnsnull) - [ ]
blocks/Types/<Block_Name>/attributes.php - [ ] Style class in
blocks/Includes/Styles/if the block needs dynamic CSS - [ ]
npm start/npm run buildandnpx gulprun cleanly — see Build & Assets