When you need to build a block theme header that sticks to the viewport on scroll, has two navigation rows (topbar + tabstrip), and includes a dark/light toggle — this skill walks through the native-first approach. Every design element uses Gutenberg block attributes. CSS only covers what blocks genuinely cannot express.
The critical insight: sticky positioning on a child element inside a WordPress template part wrapper does nothing. The template part is exactly its own height — a sticky child inside an own-height parent is a no-op. The fix is structural, not CSS.
Requires WordPress 6.2+ for native sticky position block support. The theme must have appearanceTools: true or explicit "position": { "sticky": true } in theme.json settings.
The Process
Step 1 — Enable Sticky Position Support
Add "position": { "sticky": true } to theme.json settings. This enables the sticky position UI in the block editor for Group blocks. If appearanceTools is already true, this is implicit but adding it explicitly makes intent clear.
Step 2 — Build the Header Template Part
Build parts/header.html with an outer Group (blockGap: 0 — critical) containing two child Groups. Row 1 (topbar): flex layout, space-between, with Site Title, section label, nav links as Paragraph blocks, version badge, and toggle button via Custom HTML. Row 2 (tabstrip): flex layout, Paragraph blocks with tab links. Use Paragraph links instead of Navigation blocks — simpler output, no hamburger menu.
themes/enqueue-asset · content/get · content/update
Step 3 — Wrap Header in Sticky Group (Every Template)
In every template file, wrap the template-part reference inside a Group block with "position": {"type": "sticky", "top": "0px"}. WordPress outputs is-position-sticky on the Group and generates inline CSS with position: sticky, proper top offset, and automatic admin bar handling. This must be done in every template — any template with a bare template-part tag will not have a sticky header.
Step 4 — Add CSS for Non-Block Elements
CSS handles only what blocks cannot express: backdrop-filter: blur() on the header, ::before pseudo-elements (logo dot, vertical divider), version badge pill styling, tab active state (bottom border), toggle button appearance and icon swap per theme mode, z-index override (blocks set 10, we need 100), and tabstrip hide on mobile. Do not add CSS for position: sticky, top, or admin bar offset — these are handled natively.
themes/enqueue-asset
Step 5 — Create Theme Toggle Script
Create a small JS file that toggles .light class on <html> and persists to localStorage. Enqueue in <head> (not footer) to prevent flash of wrong theme on page load. The toggle button in the header uses a Custom HTML block with onclick calling the toggle function.
themes/enqueue-asset
Step 6 — Match Header Height Token
Set --we-header-height to match the real rendered height (topbar min-height + tabstrip min-height + blockGap + borders). If this is wrong, sticky sidebars and TOC rails will misalign — they use this token for their top offset. The blockGap between rows must be 0 (explicitly set) or the default 24px inflates the header silently.
Step 7 — Delete DB Template Overrides
WordPress stores template parts in the database once edited via Site Editor — file changes are ignored. List overrides with wp post list --post_type=wp_template_part and wp_template, then delete header overrides. On multisite, check every subsite independently with --url=.
Step 8 — Verify Every Template
Curl each page type and confirm is-position-sticky is on the wrapper. Scroll in browser to verify the header stays fixed and content passes under it. Check page, single post, archive, category, search, and 404 templates. If any page doesn’t stick, check for a DB template override on that specific template.
Known Gotchas
DB template overrides are invisible. You change a file, deploy it, refresh — nothing changes. Always check wp_template and wp_template_part post types for database versions that mask your file changes.
blockGap inheritance. Any Group using flow layout inherits the global blockGap (typically 24px). Set blockGap: 0 explicitly on the header Group or the topbar and tabstrip will have invisible 24px spacing — making the header taller than your height token.
Multisite template overrides are per-site. Deploying the theme updates files, but each subsite can have its own DB overrides. You must check and clean each subsite independently.
Decision Rules
Native Group sticky vs CSS sticky. Always use the Group block’s position support at the template level. CSS position: sticky on elements inside the template part is a no-op (child sticks within own-height parent). CSS on header.wp-block-template-part works but bypasses WP’s admin bar offset handling.
Navigation block vs Paragraph links. Use Navigation blocks when you need a managed menu with hierarchical dropdowns and responsive hamburger. Use Paragraph blocks with links for fixed header nav (Blog, GitHub, Community) — simpler output, no hamburger, predictable markup.