Open-source HTML → Elementor JSON converter, packaged as both a Claude Code and an openclaw skill. Paste HTML + CSS, get a
_elementor_datapayload you can import into WordPress. Works as a standalone Python CLI or as a skill that Claude auto-invokes when you ask to "import this design into Elementor".
html2elementor is a fully open-source, local, zero-dependency-on-external-services converter that emits clean Elementor container-based JSON from static HTML + CSS.
Pairs naturally with AI-generated HTML: point an LLM at your design brief, run the output through this tool, and import the JSON into Elementor. Or install as a Claude Code skill and let Claude do all three steps for you.
- Parses HTML + CSS locally (no browser, no API calls) using BeautifulSoup + tinycss2.
- Resolves the CSS cascade including inheritance, inline styles, and CSS custom properties (
var(--x)). - Maps DOM nodes to Elementor widgets:
heading,text-editor,button,image,icon-list,icon-box. - Builds a container tree matching Elementor 3.x flex layouts (rows, columns, nested grids).
- Extracts global colors and typography into a companion
kit.json(page-unique hashed IDs so imports don't clobber each other). - Emits a
verify.pycompanion that diff-checks the output against the source and flags missing colors / wrong sizes / mis-matched backgrounds.
Input (landing.html):
<section style="padding:96px 64px;background:#0b1220;text-align:center">
<h1 style="color:white;font-size:64px">Hello world</h1>
<p style="color:#9ca3af;font-size:19px">A subtitle.</p>
<a href="#" style="background:#ec4899;color:white;padding:14px 28px;border-radius:8px">
Get started
</a>
</section>Output (landing.json):
[
{
"elType": "container",
"settings": {
"background_background": "classic",
"background_color": "#0b1220",
"padding": {"unit":"px","top":"96","right":"64","bottom":"96","left":"64"},
"flex_align_items": "stretch"
},
"elements": [
{"elType":"widget","widgetType":"heading","settings":{"title":"Hello world","title_color":"#ffffff","typography_font_size":{"unit":"px","size":"64"}}},
{"elType":"widget","widgetType":"text-editor","settings":{"editor":"<p>A subtitle.</p>","text_color":"#9ca3af"}},
{"elType":"widget","widgetType":"button","settings":{"text":"Get started","background_color":"#ec4899","button_text_color":"#ffffff"}}
]
}
]Drop into _elementor_data meta of a WordPress page → your layout renders.
git clone https://github.com/dudaster/html2elementor.git
cd html2elementor
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtRequires Python 3.10+ and three tiny dependencies: beautifulsoup4, tinycss2, cssselect2. No browser, no Node, no services.
SKILL.md at the repo root tells Claude when to invoke the tool. Clone straight into your skills folder:
git clone https://github.com/dudaster/html2elementor.git ~/.claude/skills/html2elementor
cd ~/.claude/skills/html2elementor
python3 -m venv .venv && .venv/bin/pip install -r requirements.txtAfter that, prompts like "convert landing.html to Elementor" or "import this mockup into the WP sandbox" auto-trigger the skill — Claude runs the conversion, the verifier, and the import for you.
The same SKILL.md is valid for openclaw — the frontmatter declares metadata.openclaw.requires (Python bins + deps) so openclaw can install dependencies automatically.
# From your openclaw skills folder (or symlink from anywhere)
git clone https://github.com/dudaster/html2elementor.git ~/.openclaw/skills/html2elementor
openclaw install # resolves python bin + pip deps declared in frontmatterpython3 -m html2elementor input.html -o output.jsonThis produces:
output.json— the Elementor data payloadoutput.kit.json— site-kit globals (custom colors, custom typography)
External stylesheets are loaded automatically. If input.html links <link rel="stylesheet" href="styles.css">, the converter resolves that file relative to the HTML file's directory and includes it in the CSS cascade. CSS custom properties (var(--accent)) are resolved before mapping to Elementor settings.
For designs where the stylesheet can't be auto-detected (e.g. HTML piped via stdin, or a CDN URL), pass it explicitly:
# Extra CSS file
python3 -m html2elementor input.html --css styles.css -o output.json
# Multiple CSS files
python3 -m html2elementor input.html --css reset.css --css design.css -o output.jsonfrom html2elementor import convert
with open("landing.html") as f:
html = f.read()
# Pass html_path so linked stylesheets are resolved automatically
result = convert(html, html_path="landing.html")
# result["layout"] → list[dict] (sections → containers → widgets)
# result["kit"] → dict (custom_colors + custom_typography)
# Or pass CSS strings directly
with open("styles.css") as f:
css = f.read()
result = convert(html, extra_css=[css])# 1. Copy files into the container / host
docker compose cp output.json wp:/tmp/layout.json
docker compose cp output.kit.json wp:/tmp/layout.kit.json
# 2. Merge into the active Elementor kit + create a page
docker compose exec wp wp eval '
$data = file_get_contents("/tmp/layout.json");
$kit = json_decode(file_get_contents("/tmp/layout.kit.json"), true);
$active_kit = get_option("elementor_active_kit");
$ks = get_post_meta($active_kit, "_elementor_page_settings", true) ?: [];
foreach (($kit["custom_colors"] ?? []) as $c) {
$ks["custom_colors"][] = $c;
}
foreach (($kit["custom_typography"] ?? []) as $t) {
$ks["custom_typography"][] = $t;
}
update_post_meta($active_kit, "_elementor_page_settings", $ks);
$pid = wp_insert_post([
"post_title" => "My Page",
"post_status" => "publish",
"post_type" => "page",
"meta_input" => [
"_elementor_edit_mode" => "builder",
"_elementor_template_type" => "wp-page",
"_wp_page_template" => "elementor_canvas",
],
]);
update_post_meta($pid, "_elementor_data", wp_slash($data));
echo "Page: $pid\n";
' --allow-root
# 3. Flush Elementor's CSS cache so the page renders immediately
docker compose exec wp wp elementor flush_css --allow-rootverify.py compares the generated layout against the source HTML and reports mismatches in color, font-size, spacing, and alignment. Useful during iteration:
python3 -m html2elementor.verify input.html output.json
# Widgets checked: 52
# Issues: 0
# ✓ All checks passedTolerances: font-size ±2px, padding/margin ±4px, colors exact.
| HTML | Elementor widget |
|---|---|
<h1> … <h6> |
heading with matching header_size |
<p> (long text) |
text-editor with resolved color + typography |
<p> / <div> with short text (≤50 chars) |
heading with header_size: "div" |
<button> / styled <a> |
button (filled, outlined, or text-link) |
<img> |
image (with circular / custom-size detection) |
<div> + bg + radius 50% + fixed size |
circular avatar inner container |
<div> + bg + radius + short text |
badge / pill |
<div class="col"><h4>…</h4><a>…</a>…</div> |
heading + icon-list (footer columns) |
<input> |
text-editor with inline-styled HTML input |
- Card grids —
display: gridor similar flex-row containers → Elementor row with per-column widths fromgrid-template-columns - Split hero — 2-child flex-row with no fixed widths → 2× 48% side-by-side columns
- List rows — flex-row with fixed-width label + content div (agenda/schedule slots) → single text-editor with absolute-positioned label (reliable cross-browser)
- Header navs —
<header>/<nav>with logo + links + CTA → dedicated flex-row layout with icon-list + button - Styled wrappers — any
<div>with gradient / solid non-white bg / border-radius preserved as inner container
- Cascade with specificity sorting
- Inheritance of
color,font-*,text-align,letter-spacing,text-transform - Inline
style=""attributes (highest priority) - CSS custom properties (
var(--x)) resolved from:root/html/body(with chain resolution) - Shorthand expansion:
padding,margin,border,border-radius,background - Named color keywords (
white,black,red,purple, …) - Linear gradients (parsed into Elementor gradient settings)
flex-wrap: wrap→ propagates to tablet + mobile- Row flex containers default to
flex_direction_mobile: column(stack below 768px) - Typography globals respected at all breakpoints
- Tested at desktop / tablet / mobile
Will not work well:
- JS-rendered pages (Next.js hydration, React CSR, etc.) — use the optional
--urlmode with Playwright if needed - Media queries with
@media (min-width: …)— currently no extraction - Grid tracks beyond
repeat(N, 1fr)— e.g.grid-template-columns: 2fr 1frcollapses to "N columns" - CSS animations, transitions,
transform - Pseudo-elements (
::before,::after,:hover,:focus) backdrop-filter,clip-path, complexmaskeffects
Elementor-specific gotchas:
- Elementor's lazy-load experiment hides
background-imageon containers below the fold until scroll. If your imported page shows missing section backgrounds, disable it:wp eval 'update_option("elementor_experiment-e_lazyload", "inactive");' --allow-root
image_custom_dimensiononly works with files already in the WP media library. Remote URLs fall back to natural image size.- System colors (
primary,secondary,text,accent) are site-shared.html2elementoronly usescustom_colorswith page-unique hashed IDs to prevent page A's palette from overwriting page B's.
html2elementor/
├── __init__.py # Public API: convert(html) → {layout, kit}
├── cli.py # `python3 -m html2elementor input.html -o out.json`
├── parser.py # HTML → tree of {tag, classes, text, styles, children}
├── resolver.py # CSS cascade: selector matching + specificity + var() sub
├── sections.py # Top-level section detection
├── widgets.py # DOM → widget specs (heading, button, image, …)
├── containers.py # Section → flex container settings
├── styles.py # CSS parsing helpers (padding, radius, shadow, typography)
├── colors.py # hex/rgb/named-color → #hex, darken(), lighten()
├── globals.py # Extract site-wide colors + typography into kit.json
├── hover.py # Hover state generation for buttons / icon-boxes
├── media.py # Optional image upload helpers
├── builder.py # Assemble final _elementor_data JSON (IDs, nesting)
└── verify.py # Diff output vs source (colors, sizes, spacing)
Pipeline:
HTML string
↓ parser.parse_html (BeautifulSoup + inline styles)
↓ resolver.resolve_all (cascade + var() + inheritance)
↓ sections.detect_sections
↓ containers.map_section (per section)
↓ widgets._walk (recursive DOM traversal)
↓ emit widgets with Elementor settings
↓ globals.consolidate (extract kit + rewrite references)
↓ builder.build_layout (assign IDs, nest elements)
↓ JSON
10 test pages covering common modern landing patterns:
| Test | Pattern |
|---|---|
portfolio.html |
Minimalist creative portfolio |
education.html |
Edtech / courses |
pricing.html |
SaaS pricing plans |
analytics.html |
Complex SaaS landing (10 sections) |
conference.html |
Event with agenda + speakers + sponsors |
studio.html |
Agency with left-aligned hero |
blog.html |
Magazine with split hero + image cards |
app.html |
Newsletter form with input+button |
team.html |
Team photos + values grid |
ai-saas.html |
AI-generated style (CSS vars, Tailwind-ish classes) |
Run all tests:
for t in portfolio education pricing analytics conference studio blog app team ai-saas; do
python3 -m html2elementor html2elementor/tests/$t.html -o /tmp/out.json
python3 -m html2elementor.verify html2elementor/tests/$t.html /tmp/out.json
doneCurrent status: 95% visual match on AI-generated HTML after 5 iterations of a screenshot-diff loop.
- Local, deterministic, free forever. No API keys, no services, no "AI credits."
- Prefer visual fidelity over semantic fidelity. A text-editor with inline HTML that renders correctly beats a semantically-pure widget that looks wrong.
- Never share state between pages. Every page import uses hashed-ID custom colors and typographies so a new import can't break an existing page.
- Fail loud, not silent.
verify.pycatches missing colors, wrong sizes, and structural drift before you import.
- Extract
@mediaqueries into tablet/mobile overrides - Support
grid-template-columns: 2fr 1fr(non-uniform widths) -
:hoverstate extraction for buttons - Image upload into WP media library via REST API
- Elementor template kit format (
.zipinstead of raw JSON) - VS Code extension (paste HTML → preview Elementor)
PRs welcome. The shortest path to a useful contribution:
- Pick a landing page (your own, or a competitor's exported HTML).
- Run
python3 -m html2elementor your.html -o out.json. - Import, screenshot, compare.
- If something looks wrong, find the fix in
widgets.pyorresolver.py, add a test file totests/, send a PR.
See CONTRIBUTING.md for dev setup.
MIT. Use it, fork it, resell it in your SaaS, doesn't matter — just don't pretend you wrote it.
Built by @dudaster. If this saves you money on a commercial license, consider starring the repo.