Scaffold a new ACF Composer block: ACF block, ACF Composer, block registration, Gutenberg block, block view, Blade block, InnerBlocks, block.json, block scaffold, acf:block, custom block, block category, block icon, block supports, block variants, block styles, is-style, create-block, enqueue block, block CSS, block JS, custom element, block preview parity — full custom element architecture with scoped CSS and theme variations.
Scanned 9/11/2026
Install to Claude Code
npx -y skills add hekivo/superpowers-sage --skill block-scaffolding --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Block Scaffolding?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/hekivo-block-scaffolding)More formats (shields.io, HTML) on the badges page.
---
name: superpowers-sage:block-scaffolding
description: >
Scaffold a new ACF Composer block: ACF block, ACF Composer, block registration,
Gutenberg block, block view, Blade block, InnerBlocks, block.json, block scaffold,
acf:block, custom block, block category, block icon, block supports, block variants,
block styles, is-style, create-block, enqueue block, block CSS, block JS, custom element,
block preview parity — full custom element architecture with scoped CSS and theme variations.
user-invocable: true
argument-hint: "<BlockClassName or block-slug>"
---
# Block Scaffolding — Custom Element Contract per Block
Scaffold a new ACF block with the full custom element architecture: scoped CSS,
theme variations, optional JS lifecycle, selective enqueue, and documentation.
**Announce at start:** "I'm using the block-scaffolding skill to create the block."
## When to use
- **Auto-invoked by `/building`**: after `lando acorn acf:block` produces a new stub
- **Standalone new block**: a block outside of a plan
## When NOT to use
- **Existing block that needs evolution** → use `/block-refactoring` instead.
- **Non-block UI component (Button, Card)** → Blade component, not ACF block.
## Input
$ARGUMENTS
Resolve to `{ClassName}` (PascalCase) and `{slug}` (kebab-case) before proceeding.
---
## Quick Start
```bash
# Generate block stub (PascalCase name required)
bash skills/block-scaffolding/scripts/create-block.sh HeroBlock
# → app/Blocks/HeroBlock.php
# → resources/views/blocks/hero-block.blade.php
```
---
## Hard Prerequisites
**P1. Slug produces a valid custom element tag** — `block-{slug}`. The `block-`
prefix guarantees the required hyphen.
**P2. Design system foundation:**
```
resources/css/design-tokens.css — must exist
resources/views/components/ui/ — must contain button + heading
```
**P3. BaseCustomElement in theme:**
```
resources/js/core/BaseCustomElement.js — must exist
```
---
## Operation Modes
| Mode | When | `$styles` | CSS variations |
|---|---|---|---|
| **Full** | Theme switching (light/neutral/dark) | 3 entries | 3 `.is-style-*` selectors |
| **Minimal** | Fixed appearance (footer, nav, ticker) | absent | base tokens only |
---
## Phase 0 — Generate
```bash
lando acorn acf:block {ClassName} --localize
```
Produces:
- `app/Blocks/{ClassName}.php` — controller stub (localization-ready)
- `resources/views/blocks/{slug}.blade.php` — view stub
---
## Phase 0b — Shared component inventory
Before writing the view, build a local registry of available shared components:
1. Glob `resources/views/components/*.blade.php` — list all component files
2. For each component:
a. Read its `@props` declaration → note prop names + defaults
b. Grep for `var(--)` patterns → note CSS variable names consumed
3. Build local registry:
```
{
"section-header": { props: ["eyebrow","title","align"] },
"eyebrow": { consumes: ["--eyebrow-color","--decorator-color"] },
"button": { consumes: ["--btn-bg","--btn-text"] }
}
```
This registry drives CSS generation in S1 — variable names come from the project,
not from the skill's assumptions.
**Rule:** if the block needs eyebrow + heading markup, use `<x-section-header>` (or
the equivalent shared component) — do NOT emit `<x-eyebrow>` + `<h2>` inline.
If no suitable component exists, use inline markup and note it for future extraction.
---
## Phase 0c — Form detection (conditional)
Detect whether the block embeds an HTML Forms form. Two signals — any match triggers:
1. Plan/argument description mentions: `form`, `formulário`, `contact form`, `contato`, `html forms` (case-insensitive)
2. The planned `fields()` for this block includes an `addPostObject` with `post_type` containing `html-form`
If **triggered**, load the `sage-forms` skill and its references (`blade-form-views.md`, `hf-validation.md`, `traps.md`) before continuing. After Phase 1 S2 (controller written), run the three coordinated scaffolds below. If **not triggered**, skip this phase entirely.
### 0c.1 — Form Blade view (`resources/views/forms/{form-slug}.blade.php`)
Resolve `{form-slug}`:
- If the plan names a target `html-form` post (e.g. "contact form" → `contact`), use that slug.
- Otherwise use `{slug}-form` as placeholder and prepend `{{-- TODO: rename file to match the html-form CPT post_name --}}` at the top.
Write the form view using `<x-html-forms>` + `x-form.*` components, with one `x-form.field` per ACF field declared in S2. Submit button is `<x-button type="submit">`. Do not pass `pattern` attributes; do not use `type="tel"` (use `type="text" inputmode="tel"` for phone fields). See `skills/sage-forms/references/blade-form-views.md` for the full pattern and `references/traps.md` for the rationale.
### 0c.2 — Validation module (`resources/js/modules/hf-validation.js`)
Glob check: if `resources/js/modules/hf-validation.js` exists, **skip** — one module per project, reused across forms. If absent, write the scaffold from `skills/sage-forms/references/hf-validation.md` (the "Full Module Skeleton" section).
### 0c.3 — Block JS patch (`resources/js/blocks/{slug}.js`)
Add an import at the top:
```js
import { initHfValidation } from '../modules/hf-validation';
```
Inside the block's `init()` method, add:
```js
const form = this.querySelector('.hf-form');
if (form) {
initHfValidation(form, {
messages: {
// TODO: configure per form — see skills/sage-forms/references/hf-validation.md
},
validators: {
// TODO: configure per form
},
});
}
```
### Phase 0c non-objective
Phase 0c does NOT write validator functions or localized messages — always produces `// TODO: configure per form` stubs. Validator content varies per project and per form; this is deliberate.
---
## Phase 1 — Implement S1–S4
### S1 — `resources/css/blocks/{slug}.css`
Before generating CSS, apply the background context decision table.
Read the component's `design-guide.md` (`## Tokens → Colors` section) if available:
| Token found in design-guide Colors | Background context | CSS action |
|---|---|---|
| `bg-depth`, `bg-primary`, `bg-dark`, `bg-inverse` | Dark | Override cascade vars with `*-on-dark` equivalents |
| `bg-identity`, `bg-sage`, `bg-accent` | Identity (brand color bg) | Override cascade vars with `*-on-identity` equivalents (e.g. `var(--color-identity-fg)`) |
| `bg-bg`, `bg-surface`, `bg-muted`, absent | Light (default) | No override — inherit `:root` defaults |
| Unrecognized token | Ambiguous | Generate with `/* VERIFY: background context unknown */` |
Variable names for the cascade block come from the Phase 0b registry (what each
shared component actually consumes via `var(--)` references).
**Full mode — light section (no override):**
```css
@reference "../app.css";
block-{slug} {
@apply block overflow-hidden;
/* cascade — inherited by child components */
--eyebrow-color: var(--color-identity);
--heading-color: var(--color-fg);
--body-color: var(--color-fg);
--decorator-color: var(--color-identity);
}
```
*(variable names from Phase 0b registry; values from `:root` defaults)*
**Full mode — dark section (`bg-depth` detected in design-guide):**
```css
@reference "../app.css";
block-{slug} {
@apply block overflow-hidden;
/* cascade — dark section, override :root defaults; values may differ per component — adjust per design-guide */
--eyebrow-color: var(--color-depth-fg);
--heading-color: var(--color-depth-fg);
--body-color: var(--color-depth-fg);
--decorator-color: var(--color-depth-fg);
}
```
**Full mode with `$styles` variations (background changes per variation):**
```css
@reference "../app.css";
block-{slug} {
@apply block overflow-hidden;
/* cascade — light default */
--eyebrow-color: var(--color-identity);
--heading-color: var(--color-fg);
--body-color: var(--color-fg);
--decorator-color: var(--color-identity);
}
.is-style-dark block-{slug} {
--eyebrow-color: var(--color-depth-fg);
--heading-color: var(--color-depth-fg);
--body-color: var(--color-depth-fg);
--decorator-color: var(--color-depth-fg);
}
```
**Minimal mode:** omit `.is-style-*` selectors. Include the token declarations
commented out so the developer has the vocabulary available:
```css
@reference "../app.css";
block-{slug} {
@apply block overflow-hidden;
/* --eyebrow-color: var(--color-identity); */
/* --heading-color: var(--color-fg); */
/* --body-color: var(--color-fg); */
/* --decorator-color: var(--color-identity); */
}
```
**CSS rules:**
- `@apply` for all Tailwind utilities (`block`, `overflow-hidden`, `flex`, spacing, etc.)
- CSS custom properties remain native CSS — no `@apply` equivalent exists for cascade variables
- No hardcoded color values — all values reference `@theme` tokens via `var(--)`
- `@reference` not `@import` — grants token access without duplicating the stylesheet
- `.is-style-* block-{slug}` single selector — works in editor and frontend
### S2 — `app/Blocks/{ClassName}.php`
Use template: `assets/block-atomic.php.tpl` (leaf block) or `assets/block-container.php.tpl` (InnerBlocks).
Key rules:
- Always declare `$spacing` and `$supports` — editor controls users expect
- `$styles`: Full mode only — omit entirely for Minimal blocks
- `$styles` format: `['name' => 'dark']` not `['value' => 'dark']` (WP 6.x)
- `assets()` must remain empty — enqueue via `ThemeServiceProvider::boot()`
- `fields()` declares all ACF fields — never use the ACF GUI
See [`references/acf-composer-registration.md`](references/acf-composer-registration.md) for full class structure.
For InnerBlocks containers see [`references/inner-blocks.md`](references/inner-blocks.md).
For `$styles` and variant CSS see [`references/variants.md`](references/variants.md).
### S3 — `resources/js/blocks/{slug}.js`
Always generate, even for static blocks:
```js
import BaseCustomElement from '../core/BaseCustomElement.js';
export default class Block{PascalSlug} extends BaseCustomElement {
static tagName = 'block-{slug}';
init() {
// Block behavior. Empty is valid for static blocks.
}
}
BaseCustomElement.register(Block{PascalSlug});
```
Rules: class name `Block{PascalSlug}`, `static tagName` matches CSS selector,
`init()` empty = static block, `BaseCustomElement.register()` at bottom.
### S4 — `resources/views/blocks/{slug}.blade.php`
Use template: `assets/block-view.blade.php.tpl`. Key points:
```blade
@unless ($block->preview)
<section {!! get_block_wrapper_attributes() !!}>
@endunless
<block-{slug} class="flex flex-col">
{{-- content --}}
</block-{slug}>
@unless ($block->preview)
</section>
@endunless
```
Rules:
- `@unless ($block->preview)` wraps `<section>` — skipped in editor
- `<block-{slug}>` is the CSS and JS root
- Structural utilities (flex, grid, gap, px) on `<block-{slug}>` or children
- No hardcoded colors, typography classes, or background utilities in the view
For editor vs frontend parity issues see [`references/edit-preview-parity.md`](references/edit-preview-parity.md).
For `block.json` and `editor_script` see [`references/block-json.md`](references/block-json.md).
---
## Phase 2 — Enqueue Guard (`ThemeServiceProvider::boot()`)
Search for `has_block` in `app/Providers/ThemeServiceProvider.php`.
**If pattern EXISTS:** add `'{slug}' => true,` to the `$blocks` array.
**If pattern DOES NOT EXIST:** implement it:
```php
add_action('wp_enqueue_scripts', function () {
$blocks = [
'{slug}' => true,
];
foreach (array_keys($blocks) as $slug) {
if (! has_block("acf/{$slug}")) continue;
$cssAsset = \Roots\asset("css/blocks/{$slug}.css");
if ($cssAsset->exists()) {
wp_enqueue_style("block-{$slug}", $cssAsset->uri(), [], $cssAsset->version());
}
$jsAsset = \Roots\asset("js/blocks/{$slug}.js");
if ($jsAsset->exists()) {
wp_enqueue_script("block-{$slug}", $jsAsset->uri(), [], $jsAsset->version(), true);
}
}
}, 20);
```
---
## Phase 3 — Editor CSS
Add one `@import` per block to `resources/css/editor.css`:
```css
@import './blocks/{slug}.css';
```
---
## Phase 4 — Block README (`docs/blocks/{slug}.md`)
Document: custom element name, ACF fields table, theme variations table (Full mode),
CSS tokens table, and file dependency list (controller, view, CSS, JS, enqueue, editor CSS).
**If Phase 0c triggered**, the README additionally documents:
- Form view path: `resources/views/forms/{form-slug}.blade.php`
- Validation module path: `resources/js/modules/hf-validation.js`
- DOM events handled: `hf-success`, `hf-error`
- Pointer to the `sage-forms` skill for the integration pattern
---
## Phase 5 — Verification
### Build gates:
```bash
lando theme-build # exit 0; block-{slug}-*.css AND .js must appear in output
lando flush # clear Acorn/Blade/OPcache
```
### Runtime checks:
1. Screenshot at canonical width from `plan.md`
2. Verify `<link href="*/block-{slug}-*.css">` and `<script>` in DOM
3. Custom element upgraded: `document.querySelector('block-{slug}').constructor !== HTMLElement`
4. Full mode: test each variation via DevTools adding `is-style-neutral` / `is-style-dark`
to the outer `<section>` and confirm cascade vars (e.g. `--eyebrow-color`, `--heading-color`) resolve correctly
5. `git commit -m "feat(blocks): scaffold {slug}" && git push`
---
## Critical Rules
1. **Always use `lando acorn acf:block` — never create block stubs manually.** The generator sets correct namespaces, registration hooks, and file paths.
2. **View is Blade** (`resources/views/blocks/{slug}.blade.php`). Never plain PHP.
3. **Fields via `fields()` method** — never the ACF GUI. GUI fields are lost on composer install.
4. **`get_block_wrapper_attributes()` on `<section>`** — provides accessibility, spacing, alignment, and variation classes.
5. **`assets()` stays empty** — CSS/JS enqueue belongs in `ThemeServiceProvider::boot()`.
6. **`@reference` not `@import` in block CSS** — avoids duplicating the full stylesheet.
7. **`@apply block overflow-hidden` on the custom element** — custom elements default to `inline`; `overflow-hidden` prevents bleed.
8. **Commit before declaring done** — git state is part of the Definition of Done.
---
## Anti-Drift Table
| Wrong | Correct |
|---|---|
| `<section class="b-{slug}">` + `$attributes->merge()` | `@unless preview <section {!! get_block_wrapper_attributes() !!}>` + `<block-{slug}>` |
| `.b-{slug} { ... }` | `block-{slug} { ... }` tag selector |
| `&.is-style-neutral, .is-style-neutral &` | `.is-style-neutral block-{slug}` |
| No `@apply block` on custom element | `@apply block overflow-hidden` always — custom elements default to `inline` |
| `<hero>` (no hyphen) | `<block-hero>` |
| `assets()` with enqueue | `assets()` empty — enqueue in ThemeServiceProvider |
| `@import "../app.css"` | `@reference "../app.css"` |
| `['value' => 'dark']` in `$styles` | `['name' => 'dark']` |
| Skip `$spacing` / `$supports` | Always declare both |
| `acf:block` without `--localize` | `lando acorn acf:block {Name} --localize` |
| Skip JS for static block | Generate stub with empty `init()` |
| Done without git commit | `git commit` + `git push` required |
---
## Verification
- [ ] `lando theme-build` exits 0
- [ ] Block CSS and JS files present in build output
- [ ] `<link>` and `<script>` for block assets in frontend `<head>`
- [ ] Custom element upgraded (not `HTMLElement`)
- [ ] Full mode: all three variations render correctly
- [ ] No ACF warnings in `lando wp --info` output
- [ ] `git commit` made with `feat(blocks): scaffold {slug}`
## Failure modes
### Build fails — block CSS not emitted
**Cause:** Block CSS not imported in `editor.css` or Vite entrypoint.
**Fix:** Add `@import './blocks/{slug}.css'` to `resources/css/editor.css`.
### Custom element not upgrading
**Cause:** JS not enqueued, or `BaseCustomElement.register()` missing.
**Fix:** Verify `ThemeServiceProvider::boot()` includes the slug and the JS file exists.
### Variation colors wrong on frontend
**Cause:** `<section {!! $attributes !!}>` missing — `is-style-*` class not in DOM.
**Fix:** Restore `@unless ($block->preview)` wrapper with `get_block_wrapper_attributes()`.
### ACF fields not saving
**Cause:** Fields defined in `fields()` but block registered before ACF Composer boots.
**Fix:** Ensure the block class is auto-discovered by ACF Composer (check `config/acf.php`).
For all edit/preview parity issues see [`references/edit-preview-parity.md`](references/edit-preview-parity.md).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!