← All posts

Editing raw HTML in Elementor, and when to stop

Elementor has no “view source” button, which is the first thing people go looking for. What it has is an HTML widget: drop it on a page, paste markup, and Elementor outputs it verbatim. That covers most of why you’d want raw HTML in the first place.

The more useful question is the one that comes later: when should a block of HTML stop being HTML and become real Elementor elements? That’s where pages quietly rot, and it’s the part nobody writes about.

Viewing and editing HTML in Elementor

Adding raw markup. Search “HTML” in the widget panel, drag the HTML widget in, paste. It renders exactly what you give it, including <style> and <script> tags.

Seeing a page’s actual output. Elementor doesn’t show you its own generated markup in the editor. Use your browser’s inspector on the front end, or view source. What Elementor stores internally isn’t HTML at all, it’s a JSON tree in the _elementor_data post meta, and the HTML is generated from that at render time. That distinction explains a lot of otherwise confusing behaviour.

How Elementor stores a page _elementor_data JSON tree · post meta the source of truth renders at request time HTML output on the front end generated, not stored
What Elementor stores isn't HTML at all — the HTML is generated from a JSON tree at render time.

Editing an existing HTML widget. Click it, edit the content field. There’s no syntax highlighting, so for anything substantial, write it elsewhere and paste it in.

Custom CSS belongs in the Advanced tab (Elementor Pro) or Appearance → Customize → Additional CSS, not in a <style> tag inside an HTML widget, unless the styles are genuinely local to that one block.

What the HTML widget is legitimately good for

It has real uses, and “never use it” would be bad advice:

  • Third-party embeds that ship as a script snippet: analytics, chat widgets, booking systems, maps.
  • Structured data (JSON-LD) you want on one specific page.
  • A truly one-off element that no widget covers and that you’ll never edit again.
  • Prototyping, when you want something on the page now and will rebuild it properly later.

The pattern in that list: things that are opaque anyway. A booking script isn’t content you’d want to edit visually. It’s a black box, and a black box is exactly what the HTML widget is.

The trap

The trouble starts when the HTML widget holds content rather than an embed. A pricing table. A team grid. A menu.

It works. It looks right. And then:

  • Nobody else can edit it. Your client asked for a page builder specifically so they wouldn’t need to touch code. A <div> full of prices defeats the entire purchase.
  • It doesn’t respond to your design system. Change a global colour or font, and everything on the page updates except that block, because it never participated.
  • Responsive behaviour is yours to hand-write. Elementor’s per-breakpoint controls don’t apply. You’re writing media queries by hand, in a text field, with no preview.
  • AI agents can’t work with it either. This one is new but increasingly relevant. An agent can read and restructure native elements. Given a wall of markup, the best it can do is regenerate the whole blob.
Same content, two ways to hold it HTML widget an opaque black box Native elements open, structured content no one else can edit ignores global styles responsive by hand AI can only regenerate the blob editable by anyone responds to global styles built-in responsive controls AI can restructure
An embed is opaque by nature; content shouldn't be.

None of these hurt on day one. All of them hurt in month six.

A real conversion, and what it cost

Recently we rebuilt a restaurant menu section that had been built exactly this way: a single HTML widget holding a tab bar and 28 menu rows, with inline JavaScript doing the filtering. It rendered fine.

Converting it to native Elementor meant an Elementor Pro Nested Tabs widget with six tab panels, each row a grid container holding heading widgets for number, name, description, and price. About 150 elements in total.

The result is editable by anyone, responds to global styles, and uses Elementor’s own responsive controls instead of hand-written media queries.

Two bugs surfaced during the conversion that are worth knowing about, because both are silent.

_css_classes does nothing on a container

Widgets and containers use different setting keys for the same concepts. The original section set _css_classes on a container, which is the widget key. Containers use css_classes, with no underscore.

Elementor accepted the value without complaint and stored it. It simply never rendered the class. The CSS targeting that class had never applied, on a live page, for months, and nothing anywhere reported a problem.

One concept, two keys _css_classes widget key css_classes container key Elementor stores any key you send; a wrong key looks like it worked.
If a setting seems to have no effect, suspect the key before the value.
ConceptOn a widgetOn a container
CSS classes_css_classescss_classes
Position_positionposition
Custom width_element_custom_widthwidth

This generalises past this one bug: Elementor stores any settings key you send. A wrong key looks like it worked. If a setting appears to have no effect, suspect the key before you suspect the value.

Grid columns and mobile don’t inherit the way you’d expect

The rows were CSS grid, 60px 1fr 1.2fr 100px on desktop, narrowing to two columns on mobile. We hid the description on mobile but forgot the row number, so the number took column one and squeezed the name into the 80px price column, wrapping the price onto its own line.

The lesson isn’t “remember to hide things”. It’s that a responsive grid needs its column count and its hidden children changed together. Change one without the other and the layout doesn’t break loudly, it just goes subtly wrong on a screen size you aren’t looking at.

When to convert, and when to leave it

A rough test: would anyone ever want to edit this without a developer?

Leave it as HTMLConvert to native elements
Third-party embed scriptsAnything with words a client will change
JSON-LD structured dataPrices, names, dates, hours
One-off decorative flourishAnything repeated more than three times
Something you’ll delete next monthAnything that should match global styles

Prices are the giveaway. Prices change. If changing a price means editing markup, that section is in the wrong format.

Converting without doing it by hand

150 elements is a tedious afternoon of clicking. This is genuinely good work for an AI agent connected to a WordPress MCP server: you describe the structure, it builds the containers and widgets, and you check the result in the editor.

That’s how the menu above was rebuilt, and it’s why both bugs got found. Building it programmatically forces every setting to be explicit, and explicit settings are testable in a way that “it looks right” never is.

← All postsSubscribe via RSS