Building a Modern WordPress Theme Without Elementor

Quick Summary

  • Replace a page builder with template parts per section, ACF Flexible Content for editing, and scripts loaded only on pages that contain their section.
  • Writing templates is the easy half; the traps are Elementor Pro's Theme Builder overriding headers, content in builder meta, delayed JavaScript and changed URLs.
  • List pages with _elementor_edit_mode set using WP-CLI, 301 moved URLs, return 410 for removed ones, and keep builder meta until new pages are live.

The header on my new WordPress theme without Elementor was wrong, and the bug wasn’t in the theme. Elementor Pro’s Theme Builder was still active, and it was quietly swapping in the header and footer it had always served.

Nothing errored and nothing warned. That’s the part of leaving a page builder nobody mentions: writing the templates is the easy half. The traps are in the migration, in plugins that keep doing their job after you’ve stopped wanting them to, content stored in places you don’t look, and URLs that change without anyone deciding they should.

A WordPress theme without Elementor isn’t about hating page builders

I still build Elementor sites when a project calls for one. A client who wants to rearrange pages without a developer, a budget that doesn’t cover custom templates, a team that already knows the tool: those are good reasons.

My own site had outgrown it. It ran Elementor Pro plus Happy Addons, Essential Addons and the Jet plugin family, alongside a caching plugin and a tables plugin. Each one made sense on the day it was installed. Together they meant the markup was something I negotiated with rather than something I wrote.

A WordPress theme without Elementor gives three things back:

  • Markup you control. Every element is in a template you can read, so an accessibility fix or a design tweak is an edit, not a workaround.
  • Fewer plugins. Not as a purity test. Each plugin is code you didn’t review and updates you have to trust.
  • Assets only where they’re used. A builder has to be ready for any widget on any page. A theme knows which sections a page contains and can load exactly that.

“Fewer” doesn’t mean “none”. On my site, a drag-and-drop post ordering plugin had a bug that squeezed the admin list’s Title column to about 50px: it copied column widths from the first row’s <td> cells, but WordPress renders the title cell as a <th>, so every width landed one column off. A few lines of admin script fixed it, and the plugin stayed, because it does its one job well.

What replaces the builder in a custom WordPress theme

A page builder quietly does three jobs: layout, editing and asset loading. Each gets a plain replacement.

Layout becomes template parts: one PHP file per design section. Editing becomes ACF Flexible Content, where the editor stacks those sections on a page and fills in their fields; the reasoning is in why I still use ACF for custom sites. Content that appears on many pages, like pricing or an FAQ, moves to an ACF options page so it’s edited once.

On my site the design already existed as a static HTML/CSS/JS prototype, so its CSS and JavaScript were kept exactly as they were, and WordPress-specific styles went into a separate small file. That separation pays off every time something looks wrong: you know straight away whether to look at the design or at what WordPress added. The full folder layout is in how I structure a WordPress project.

Asset loading is the job people forget. ACF stores a Flexible Content field’s layout names, in order, as a plain array in post meta, so the theme can check which sections a page uses without loading ACF at all:

PHP
function mytheme_has_layout( $layout, $post_id = 0 ) {
	$post_id = $post_id ?: get_queried_object_id();
	$layouts = get_post_meta( $post_id, 'sections', true ); // e.g. [ 'hero_home', 'services', 'faq' ]

	return is_array( $layouts ) && in_array( $layout, $layouts, true );
}

add_action( 'wp_enqueue_scripts', function () {
	if ( is_singular() && mytheme_has_layout( 'hero_home' ) ) {
		wp_enqueue_script(
			'mytheme-home',
			get_theme_file_uri( 'assets/js/home.js' ),
			[],
			wp_get_theme()->get( 'Version' ),
			[ 'strategy' => 'defer' ]
		);
	}
} );

The hero’s script now loads only on pages that contain the hero, and the same check covers every heavy section. The strategy argument tells wp_enqueue_script() to print the defer attribute itself, so there’s no need to filter script tags by hand.

The traps are in the migration

The builder keeps building after you stop using it

Theme Builder templates don’t care which theme is active. As long as Elementor Pro is running and a header or footer template’s display conditions match, it replaces yours. Switching Elementor Pro off was the fix. The lesson is where to look: if a header or footer still looks like the old site, check the builder’s theme templates before you debug your own code.

Your content lives in two places

Elementor keeps the real page in post meta: _elementor_data holds the layout as JSON, and _elementor_edit_mode marks the page as built with Elementor. It also writes a stripped-down copy into the normal post content. Deactivate the plugin and pages fall back to that copy: text and images, none of the layout.

Before deciding anything, get the list. WP-CLI passes query arguments straight through, so one command shows every page that still depends on the builder:

Shell
wp post list --post_type=page --meta_key=_elementor_edit_mode --meta_value=builder --fields=ID,post_title,post_status

Then decide page by page. Rebuild it as sections, or keep the plain content as a simple page. And don’t bulk-delete builder meta until the new pages are live and you have a backup you’ve actually tested. That meta is the only complete record of what the old pages were.

Optimization plugins can break an interactive design

A caching plugin on my site was set to delay all JavaScript until the visitor’s first interaction. It’s a common speed trick on page builder sites. On a design with a page loader and scroll animations, it meant nothing ran until someone touched the page, so the loader and the animations broke. That plugin had to be switched off too.

Deferring scripts is fine and usually right; I’ve written about fixing defer parsing without a plugin. Delaying them until interaction is a different thing, and it’s rarely worth it when the first screen is itself interactive.

URLs change when you’re not looking

My project pages moved from /projects/ to /project/ during the rebuild (a custom post type slug change). Every old pattern (the archive, each project, each category) got a permanent 301 redirect, and the SEO plugin’s stored URLs were reset, because it caches permalinks and wouldn’t have picked up the new base by itself.

Two other URL changes were deliberate. The contact form was removed, so /contact/ now 301s to my Upwork profile. And leftover WooCommerce pages (shop, cart, checkout, my account) return 410 Gone, because the site has no store. Before you switch, list every URL the old site answers. Each one should work, redirect, or say it’s gone on purpose.

A lean checklist for migrating to a WordPress theme without Elementor

  1. List every active plugin and the one job it does. No job, no plugin.
  2. Replace small jobs with small code. My analytics tag moved from a code-snippets plugin into the theme, and code highlighting moved from a plugin to self-hosted Prism.js. Some plugins simply went: one that allowed SVG uploads, and a consent API plugin with no consent banner to talk to.
  3. Build and test the new theme on staging before the live switch.
  4. Disable the builder’s theme templates, or the builder itself, before judging your header and footer.
  5. Turn off any “delay JavaScript” option in your optimization plugin, then check every animation.
  6. Map old URLs: 301 for moved, 410 for gone. Then reset whatever your SEO plugin caches.
  7. Back up, and remove builder meta only once the new pages are live.
  8. Compare screenshots of the new theme against the design at desktop and phone widths.

The rebuild itself, including how much of this an AI coding agent handled and what it got wrong, is in I built a WordPress theme with AI.

None of this is exotic. It’s the work a page builder was quietly doing, or quietly getting in the way of, made visible. Once it’s visible, you own it, and owning the output was the reason to leave.

Share on X (opens in a new tab) Share on LinkedIn (opens in a new tab) Share on Facebook (opens in a new tab)

ChatGPT (opens in a new tab) Claude (opens in a new tab) Gemini (opens in a new tab) Perplexity (opens in a new tab) Grok (opens in a new tab)

More Insights

See More