Use this when adding or changing blocks under blocks/. Registration: blocks/blocks.php. Scaffold: npm run gen -- --name=my-block --ns=nextora.
- Presets first — Colors, fonts, spacing, and layout widths come from
theme.json. Enablesupports.color,supports.spacing, andsupports.typographyinblock.jsonso Global Styles apply in the editor and viaget_block_wrapper_attributes()on the front end. - CSS variables — In
style.css/ block CSS, prefervar(--wp--preset--color--*),var(--wp--preset--font-size--*),var(--wp--preset--spacing--*), and shared--nextora-*tokens fromresources/css/app.css@theme. Avoid raw hex unless mapping a user-picked override (below). - Tailwind in markup — Editor preview and PHP may use utility classes (
text-contrast,bg-base) only when they mirror presets; new block-specific layout belongs inblocks/<name>/style.csswith@layerif added to the global bundle via@source. - Build — After
style.css/view.tschanges:npm run build:blocks(andnpm run build:cssif the block is scanned fromapp.css@source). Do not commit hand-editedindex.js/index.asset.php.
Use native supports.color when the whole block wrapper should follow Global Styles (typical content blocks).
Add custom colour attributes + PanelColorSettings when colour applies to a part of the block only — e.g. icon surface, carousel chrome, image band, overlay text, button fill inside a group block. Follow § Custom colour options below.
Any block that adds sidebar Colors controls for scoped areas must follow this pattern. Reference implementation: blocks/advanced-icon/ — full walkthrough in docs/blocks/Theme Icon Block.md § Custom color settings.
WordPress treats backgroundColor, textColor, and (in some contexts) borderColor as block-support attribute names. If you reuse them for an inner surface, badge, or icon background, the editor can add has-{slug}-background-color has-background to the block list wrapper — wrong element, often stretched into a tall coloured bar in flex/stack layouts.
| Do not use for scoped overrides | Use instead |
|---|---|
backgroundColor |
surfaceBackgroundColor, imageAreaBackground, bandBackgroundColor, overlayBackgroundColor, … |
textColor |
labelColor, iconColor, overlayTextColor, numberColor, … |
borderColor (when not WP wrapper border) |
surfaceBorderColor, frameBorderColor, … |
Rule: prefix or scope the name (surface*, icon*, overlay*, band*) so it is clearly not the block wrapper’s Global Styles colour.
When colours are custom (PanelColorSettings on inner elements), disable native colour support on the same block:
"supports": {
"color": {
"background": false,
"text": false,
"link": false
}
}Only enable supports.color.background / text if the entire block wrapper is what should change — and do not duplicate that with a custom attribute of the same name.
| User picks in editor | Save in attribute | PHP / CSS output |
|---|---|---|
| Theme preset “Secondary” | slug: "secondary" |
var(--wp--preset--color--secondary) |
| Custom colour (hex) | "#e50a46" |
#e50a46 |
| Theme preset “Secondary” with opacity < 100% | "#e50a4680" (8-digit hex with alpha) |
#e50a4680 (preserve alpha) |
| Theme gradient “Primary” | slug: "gradient-primary" |
var(--wp--preset--gradient--gradient-primary) |
| Custom gradient CSS | "linear-gradient(135deg, …)" |
linear-gradient(135deg, …) |
Never store resolved hex for a theme preset. If the user selects Secondary, save "secondary", not #FFC247. Otherwise Browse styles → Color variations will not update the block when the palette changes.
PanelColorSettings onChange often returns hex even for theme swatches — normalize to slug before setAttributes.
8-digit hex with alpha: enableAlpha causes onChange to return #RRGGBBFF (8-digit with full-opacity alpha ff). The hex→slug normalizer must strip alpha before comparing against palette entries (palette only has 6-digit hex). Only convert to slug when alpha = ff; keep the raw 8-digit hex when alpha < ff (to preserve intentional transparency).
Gradient slugs: onGradientChange from PanelColorSettings returns CSS gradient strings. Normalize to slug when the CSS matches a theme gradient preset. Resolve slug back to CSS string for the gradientValue prop (to highlight the correct swatch).
| Requirement | Detail |
|---|---|
| Control | PanelColorSettings in a Colors panel (same as sibling blocks — not a one-off ColorPicker in Layout unless an existing reference block does that) |
| onChange | Run through normalizeColorForStorage(value, palette) — map theme hex → slug; keep custom hex |
| value | Run through colorValueForPicker(stored, currentPalette, lookupPalette) — show current palette hex for a stored slug so the swatch highlights correctly |
| Palette lookup | Merge active editor palette + all style-variation entries (localize from PHP via register-editor.php when hex→slug migration is needed) |
| Migration | On block load: legacy reserved names → scoped names; legacy hex → slug when identifiable |
| enableAlpha | Always pass enableAlpha to PanelColorSettings so the colour picker shows an opacity slider — PanelColorSettings should be <PanelColorSettings enableAlpha title={…} colorSettings={…} />. Without it, only a 6-digit hex picker is shown and alpha/transparency cannot be set. |
| Gradient tabs | Pass gradients={themeGradients} to PanelColorSettings and add gradientValue/onGradientChange to the colour setting entry that supports gradients. This enables the native Solid | Gradient tabs. Also set disableCustomGradients={false} to show the manual gradient builder. |
| Gradient storage | Use normalizeGradientForStorage(value, gradients) on onGradientChange — map CSS gradient string → slug; use gradientValueForPicker(slug, gradients) to resolve slug → CSS string for the gradientValue prop. |
| i18n | Labels via __('Background color', 'nextora'), etc. |
Reusable helpers live in blocks/advanced-icon/color-utils.ts — copy or extract shared utilities when adding a second block with the same pattern.
| Requirement | Detail |
|---|---|
| Resolve | Map slug → var(--wp--preset--color--{slug}); pass through custom hex (including 8-digit #RRGGBBAA with alpha); handle var:preset|color|* if present. Important: check 8-digit hex before sanitize_hex_color() since WP core only accepts 3/6-digit hex. |
| Legacy hex | Optionally map old saved hex back to slug by scanning theme + child + style-variation palettes (see nextora_icon_hex_to_preset_slug() in blocks/advanced-icon/lucide.php) |
| Apply colour | On the target inner element (BEM modifier or CSS custom property), not on get_block_wrapper_attributes() unless the whole block is the target |
| Fallback | Read scoped attribute first, then legacy name: $attributes['surfaceBackgroundColor'] ?? $attributes['backgroundColor'] ?? '' |
When writing a *_resolve_color() function in block render.php, follow this order to avoid silently dropping 8-digit hex alpha colors:
var:preset|color|slug→var(--wp--preset--color--slug)- 8-digit hex →
preg_match('/^#[0-9a-fA-F]{8}$/', $raw)— return as-is BEFOREsanitize_hex_color() - 3/6-digit hex →
sanitize_hex_color($raw) - Preset slug →
var(--wp--preset--color--slug) var(...)passthrough (if used)- Return empty string
function nextora_xxx_resolve_color( string $raw ): string {
$raw = trim( $raw );
if ( '' === $raw ) {
return '';
}
if ( preg_match( '/^var:preset\|color\|([a-z0-9_-]+)$/i', $raw, $m ) ) {
return 'var(--wp--preset--color--' . sanitize_html_class( strtolower( $m[1] ) ) . ')';
}
// 8-digit hex with alpha — MUST come before sanitize_hex_color()
if ( preg_match( '/^#[0-9a-fA-F]{8}$/', $raw ) ) {
return $raw;
}
$hex = sanitize_hex_color( $raw );
if ( is_string( $hex ) && '' !== $hex ) {
return $hex;
}
// fallback to preset slug
if ( preg_match( '/^[a-z0-9-]+$/', $raw ) ) {
return 'var(--wp--preset--color--' . sanitize_html_class( $raw ) . ')';
}
return '';
}Rationale: WordPress sanitize_hex_color() only validates #RGB and #RRGGBB. An 8-digit hex like #eaf6f624 is rejected (returns null/empty), causing the color to be silently dropped on the frontend. Checking 8-digit hex first preserves alpha transparency.
When the coloured element sits inside flex, stack, or columns, prevent stretch:
.wp-block-nextora-{slug} {
align-items: flex-start;
align-self: flex-start;
width: fit-content;
max-width: 100%;
}
/* inner surface / band */
.nextora-{slug}__surface {
flex: 0 0 auto;
width: fit-content;
height: fit-content;
}Editor-only: reset accidental has-*-background-color on the block list wrapper if legacy content still carries reserved attribute names — see blocks/advanced-icon/editor.css.
- Attribute names are scoped — not
backgroundColor/textColorfor inner/surface colours -
supports.color.*allfalse(or native support only for wrapper — not both) -
PanelColorSettingsin Colors panel; labels match sibling blocks - Store preset slugs (colors and gradients);
normalizeColorForStorage/normalizeGradientForStorageon change; resolve slug in PHP - Picker
valueuses current palette hex for stored slug;gradientValueusesgradientValueForPicker -
normalizeColorForStoragestrips alpha for palette comparison; only stores slug when alpha =ff - Colour rendered on correct inner element / CSS variable — not block wrapper background
- Legacy attribute + hex migration where templates already exist
-
width: fit-content/align-self: flex-startwhen parent is flex/stack/column - Verify in Site Editor: swatch matches, no tall background bar, colour updates after style variation change
Add custom attributes only when Global Styles / supports.color on the wrapper are not enough.
| Convention | Detail |
|---|---|
| Naming | Scoped names only — see § Custom colour options. |
| Sidebar | Panel Colors or Appearance; labels match sibling blocks. |
| Help text | One line: what the control affects; empty = theme default. |
| Front end | CSS variable or BEM modifier on the affected element — not the whole block wrapper unless intentional. |
Every content block that benefits from motion should expose an editor toggle and honor reduced motion.
| Item | Standard |
|---|---|
| Attribute | enableScrollAnimation — boolean, default true. |
| Sidebar | Panel Animation → ToggleControl label Animate on scroll (nextora). Help: Fade or move content in when it enters the viewport. Disabled automatically when the visitor prefers reduced motion. |
| PHP | When enabled, add data-nextora-scroll-reveal="1" on the block root (or a dedicated inner wrapper). When disabled, omit the attribute. |
| JS | In view.ts / view.js: run only if data-nextora-scroll-reveal="1"; use GSAP + ScrollTrigger (see blocks/team-section/view.ts) with once: true and prefers-reduced-motion: reduce → no animation. |
| Init guard | data-nextora-{block}-scroll-init="1" after setup to avoid double init. |
Existing reference: team-section / arc-gallery-section — scroll reveal pattern.
Blocks that depend on JS for layout must avoid FOUC / broken first paint.
| Phase | Standard |
|---|---|
| Markup | Stable dimensions in CSS (aspect-ratio, min-height, overflow: hidden) so space is reserved before init. |
| Loading | Root class nextora-{slug}--loading until init completes; then nextora-{slug}--ready. Optional subtle opacity or skeleton in style.css. |
| Pending | data-nextora-{slug}-init-pending="1" while waiting (e.g. width measurement); remove when done. |
| Done | data-nextora-{slug}-inited="1" (or block-specific name already in use, e.g. data-swiper-inited). |
| Init | Idempotent initRoot(); DOMContentLoaded + optional custom event for re-render (e.g. nextora-image-gallery-reinit). |
| Motion | Respect prefers-reduced-motion; carousel may still init but skip autoplay / aggressive transitions. |
Reference: blocks/team-section/view.ts (swiperInitPending, width polling before mount).
When adding controls, align with existing blocks:
| Control type | Align with |
|---|---|
| Layout preset | Image gallery grid — SelectControl + layout class nextora-{prefix}--layout-{slug}. |
| Icon-only actions | Header utilities — 2.5rem ghost control, rounded-md, currentColor. |
| Modals / search | Spotlight search — data-nextora-modal, shared modal CSS. |
| Spacing / align | supports.align wide / full; category: theme for site chrome blocks, media / design for content as appropriate. |
| Text domain | Always nextora. Block name: nextora/{slug}. |
When two or more blocks share the same type of UI element, they must use the same visual styling — identical tokens, sizes, spacing, and behaviour. Before building a new variant, check whether an existing block already has the component and copy its approach.
| Component | Reference block(s) | What to match |
|---|---|---|
| CTA / "View All" / action button | Team section, Hero section, Call to action | Font size (--wp--preset--font-size--small), weight (600), border width (1.5px on outline), padding (0.625rem 1.75rem), radius (50px), hover opacity (0.9), icon class + sizing, CSS variable naming |
| Carousel arrows | Team section, Testimonial carousel | aria-label wording, SVG chevron paths, button dimensions, hover/focus states |
| Pagination dots | Testimonial carousel | Bullet size (7px), active width (20px), transition (width + border-radius + background), ARIA |
| Card / grid hover | Post grid | Image scale amount (1.02–1.04), transition duration (0.3–0.4s), title color shift |
| Scroll reveal | Testimonial carousel, Team section | --reveal-pending/--reveal-ready class system, GSAP timeline (power3.out, 0.95s per element, 0.12s stagger), fallback timeout (1800ms) |
Sidebar controls for shared components must also match: use the same panel title, control labels, and help text as the reference block. If the reference uses PanelColorSettings for a colour, the new block must too — not inline ColorPicker in a PanelBody.
Before shipping, compare sidebar structure and help strings with the closest existing block; reuse panel titles (Settings, Layout, Animation, Colors).
-
block.json:textdomainnextora, sensiblesupports,enableScrollAnimationif content animates on scroll. -
render.php: escaped output,get_block_wrapper_attributes(), scroll data attribute when enabled. -
style.css: presets /--wp--preset--*; loading/ready states ifviewScriptpresent. -
edit.tsx: Animation toggle; colour overrides follow § Custom colour options ifPanelColorSettingsis added. -
view.ts(if needed): scroll reveal + init loading pattern; reduced motion. -
npm run build:blocks(andnpm run buildif CSS/TS global).
| Block | Pattern |
|---|---|
nextora/scrolling-promotion |
CSS-only marquee, render.php, no viewScript; items repeater in editor |
nextora/arc-gallery-section |
Arc layout + arc-math.ts / PHP mirror, live editor preview, view.ts scroll reveal |
nextora/team-section |
Swiper member carousel, members[] repeater, scroll reveal; see docs/blocks/Our Team Section Block.md |
nextora/testimonial-carousel |
Fade testimonial carousel, testimonials[] repeater, trust avatars; see docs/blocks/Testimonial Carousel.md |
nextora/vertical-showcase |
Vertical tabbed service list with autoplay image showcase and progress rail; see docs/blocks/Vertical Showcase.md |
nextora/testimonials |
Split-screen portrait + quote carousel, ring pagination; see docs/blocks/testimonials.md |
nextora/instagram-feed |
Curated Instagram tiles, Swiper carousel, lightbox modal; see docs/blocks/Instagram.md |
nextora/contact-form |
Get-in-touch form, REST submit, dual email, Tiptap message field; see docs/blocks/Contact Form Block.md |
nextora/google-maps |
Iframe or JS API map embed, markers, directions; see docs/blocks/Google Maps Block.md |
nextora/advanced-icon |
Lucide/upload icon, stacked/framed surfaces, custom colour pattern (slug storage, scoped attrs); see docs/blocks/Theme Icon Block.md |
nextora/advanced-button |
Advanced CTA buttons or icon-only items with Lucide/upload icons; see docs/blocks/Advanced Button.md |
nextora/box-icon |
Icon cards in slider or desktop grid (responsive always carousel); see docs/blocks/Box Icon.md |
AGENTS.md— theme overview and build pipeline.docs/accessibility.md— WCAG patterns and block a11y checklist.docs/modal.md,docs/spotlight-search.md— shared UI patterns.- Skill:
.cursor/skills/nextora-add-theme-block/SKILL.md - Skill:
.cursor/skills/nextora-theme-styling-and-tokens/SKILL.md - Block specs:
docs/blocks/Our Team Section Block.md,docs/blocks/Testimonial Carousel.md(./blocks/Testimonial%20Carousel.md),docs/blocks/Theme Icon Block.md,docs/blocks/Advanced Button.md