{"id":"ce85e2fc-7f22-4f84-a41e-dd1fd42112ae","slug":"html2elementor","kind":"skill","name":"html2elementor","summary":"Convert HTML + CSS to Elementor JSON (importable in WordPress). Works as a Claude Code / openclaw skill or standalone Python CLI. Local, no browser, no APIs. Handles AI-generated HTML, Tailwind mockup","owner":"dudaster","repoUrl":"https://github.com/dudaster/html2elementor","license":"MIT","licenseTier":"permissive","trust":"community","stars":16,"match":null,"fewStars":false,"format":"skill","createdAt":null,"addedAt":"2026-10-14T06:28:06.547Z","logoUrl":"https://avatars.githubusercontent.com/u/8319178?u=38b1d5440d369734bf0eeb84a07eb56fc22cf4a9&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["browser-web","developer-tools"],"tags":["elementor","html-converter","html-to-elementor","python","wordpress","elementor-converter","claude-code","elementor-template"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-14T08:57:07.722Z","sample":false,"url":"https://conduits.dev/tools/html2elementor","language":"Python","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# html2elementor — HTML → Elementor skill for Claude Code / openclaw\n\n> **Open-source HTML → Elementor JSON converter, packaged as both a Claude Code and an openclaw skill.** Paste HTML + CSS, get a `_elementor_data` payload 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\".\n\n[![Claude Code skill](https://img.shields.io/badge/claude%20code-skill-d97757.svg)](https://docs.anthropic.com/en/docs/claude-code)\n[![openclaw skill](https://img.shields.io/badge/openclaw-skill-6366f1.svg)](https://github.com/humanlayer/claude-code-skills)\n[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n[![Elementor](https://img.shields.io/badge/elementor-3.x+-ec4899.svg)](https://elementor.com/)\n\n`html2elementor` is a **fully open-source, local, zero-dependency-on-external-services** converter that emits clean Elementor container-based JSON from static HTML + CSS.\n\nPairs 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.\n\n---\n\n## What it does\n\n- Parses HTML + CSS **locally** (no browser, no API calls) using BeautifulSoup + tinycss2.\n- Resolves the CSS cascade including inheritance, inline styles, and **CSS custom properties** (`var(--x)`).\n- Maps DOM nodes to Elementor widgets: `heading`, `text-editor`, `button`, `image`, `icon-list`, `icon-box`.\n- Builds a container tree matching Elementor 3.x flex layouts (rows, columns, nested grids).\n- Extracts **global colors and typography** into a companion `kit.json` (page-unique hashed IDs so imports don't clobber each other).\n- Emits a `verify.py` companion that diff-checks the output against the source and flags missing colors / wrong sizes / mis-matched backgrounds.\n\n### Example\n\n**Input** (`landing.html`):\n```html\n<section style=\"padding:96px 64px;background:#0b1220;text-align:center\">\n  <h1 style=\"color:white;font-size:64px\">Hello world</h1>\n  <p style=\"color:#9ca3af;font-size:19px\">A subtitle.</p>\n  <a href=\"#\" style=\"background:#ec4899;color:white;padding:14px 28px;border-radius:8px\">\n    Get started\n  </a>\n</section>\n```\n\n**Output** (`landing.json`):\n```json\n[\n  {\n    \"elType\": \"container\",\n    \"settings\": {\n      \"background_background\": \"classic\",\n      \"background_color\": \"#0b1220\",\n      \"padding\": {\"unit\":\"px\",\"top\":\"96\",\"right\":\"64\",\"bottom\":\"96\",\"left\":\"64\"},\n      \"flex_align_items\": \"stretch\"\n    },\n    \"elements\": [\n      {\"elType\":\"widget\",\"widgetType\":\"heading\",\"settings\":{\"title\":\"Hello world\",\"title_color\":\"#ffffff\",\"typography_font_size\":{\"unit\":\"px\",\"size\":\"64\"}}},\n      {\"elType\":\"widget\",\"widgetType\":\"text-editor\",\"settings\":{\"editor\":\"<p>A subtitle.</p>\",\"text_color\":\"#9ca3af\"}},\n      {\"elType\":\"widget\",\"widgetType\":\"button\",\"settings\":{\"text\":\"Get started\",\"background_color\":\"#ec4899\",\"button_text_color\":\"#ffffff\"}}\n    ]\n  }\n]\n```\n\nDrop into `_elementor_data` meta of a WordPress page → your layout renders.\n\n---\n\n## Installation\n\n```bash\ngit clone https://github.com/dudaster/html2elementor.git\ncd html2elementor\npython3 -m venv .venv\nsource .venv/bin/activate\npip install -r requirements.txt\n```\n\nRequires Python 3.10+ and three tiny dependencies: `beautifulsoup4`, `tinycss2`, `cssselect2`. No browser, no Node, no services.\n\n### Install as a Claude Code skill\n\n`SKILL.md` at the repo root tells Claude when to invoke the tool. Clone straight into your skills folder:\n\n```bash\ngit clone https://github.com/dudaster/html2elementor.git ~/.claude/skills/html2elementor\ncd ~/.claude/skills/html2elementor\npython3 -m venv .venv && .venv/bin/pip install -r requirements.txt\n```\n\nAfter 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.\n\n### Install as an openclaw skill\n\nThe same `SKILL.md` is valid for [openclaw](https://github.com/humanlayer/claude-code-skills) — the frontmatter declares `metadata.openclaw.requires` (Python bins + deps) so openclaw can install dependencies automatically.\n\n```bash\n# From your openclaw skills folder (or symlink from anywhere)\ngit clone https://github.com/dudaster/html2elementor.git ~/.openclaw/skills/html2elementor\nopenclaw install  # resolves python bin + pip deps declared in frontmatter\n```\n\n---\n\n## Usage\n\n### CLI\n\n```bash\npython3 -m html2elementor input.html -o output.json\n```\n\nThis produces:\n- `output.json` — the Elementor data payload\n- `output.kit.json` — site-kit globals (custom colors, custom typography)\n\n**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.\n\nFor designs where the stylesheet can't be auto-detected (e.g. HTML piped via stdin, or a CDN URL), pass it explicitly:\n\n```bash\n# Extra CSS file\npython3 -m html2elementor input.html --css styles.css -o output.json\n\n# Multiple CSS files\npython3 -m html2elementor input.html --css reset.css --css design.css -o output.json\n```\n\n### Python API\n\n```python\nfrom html2elementor import convert\n\nwith open(\"landing.html\") as f:\n    html = f.read()\n\n# Pass html_path so linked stylesheets are resolved automatically\nresult = convert(html, html_path=\"landing.html\")\n# result[\"layout\"]     → list[dict] (sections → containers → widgets)\n# result[\"kit\"]        → dict (custom_colors + custom_typography)\n\n# Or pass CSS strings directly\nwith open(\"styles.css\") as f:\n    css = f.read()\nresult = convert(html, extra_css=[css])\n```\n\n### Importing into WordPress\n\n```bash\n# 1. Copy files into the container / host\ndocker compose cp output.json     wp:/tmp/layout.json\ndocker compose cp output.kit.json wp:/tmp/layout.kit.json\n\n# 2. Merge into the active Elementor kit + create a page\ndocker compose exec wp wp eval '\n$data = file_get_contents(\"/tmp/layout.json\");\n$kit  = json_decode(file_get_contents(\"/tmp/layout.kit.json\"), true);\n$active_kit = get_option(\"elementor_active_kit\");\n$ks = get_post_meta($active_kit, \"_elementor_page_settings\", true) ?: [];\nforeach (($kit[\"custom_colors\"] ?? []) as $c) {\n    $ks[\"custom_colors\"][] = $c;\n}\nforeach (($kit[\"custom_typography\"] ?? []) as $t) {\n    $ks[\"custom_typography\"][] = $t;\n}\nupdate_post_meta($active_kit, \"_elementor_page_settings\", $ks);\n\n$pid = wp_insert_post([\n    \"post_title\"  => \"My Page\",\n    \"post_status\" => \"publish\",\n    \"post_type\"   => \"page\",\n    \"meta_input\"  => [\n        \"_elementor_edit_mode\"      => \"builder\",\n        \"_elementor_template_type\"  => \"wp-page\",\n        \"_wp_page_template\"         => \"elementor_canvas\",\n    ],\n]);\nupdate_post_meta($pid, \"_elementor_data\", wp_slash($data));\necho \"Page: $pid\\n\";\n' --allow-root\n\n# 3. Flush Elementor's CSS cache so the page renders immediately\ndocker compose exec wp wp elementor flush_css --allow-root\n```\n\n### Verify the conversion\n\n`verify.py` compares the generated layout against the source HTML and reports mismatches in color, font-size, spacing, and alignment. Useful during iteration:\n\n```bash\npython3 -m html2elementor.verify input.html output.json\n# Widgets checked: 52\n# Issues: 0\n# ✓ All checks passed\n```\n\nTolerances: font-size ±2px, padding/margin ±4px, colors exact.\n\n---\n\n## What gets converted\n\n### Widget mapping\n\n| HTML | Elementor widget |\n|------|------------------|\n| `<h1>` … `<h6>` | `heading` with matching `header_size` |\n| `<p>` (long text) | `text-editor` with resolved color + typography |\n| `<p>` / `<div>` with short text (≤50 chars) | `heading` with `header_size: \"div\"` |\n| `<button>` / styled `<a>` | `button` (filled, outlined, or text-link) |\n| `<img>` | `image` (with circular / custom-size detection) |\n| `<div>` + bg + radius 50% + fixed size | **circular avatar** inner container |\n| `<div>` + bg + radius + short text | **badge** / pill |\n| `<div class=\"col\"><h4>…</h4><a>…</a>…</div>` | `heading` + `icon-list` (footer columns) |\n| `<input>` | `text-editor` with inline-styled HTML input |\n\n### Layout detection\n\n- **Card grids** — `display: grid` or similar flex-row containers → Elementor row with per-column widths from `grid-template-columns`\n- **Split hero** — 2-child flex-row with no fixed widths → 2× 48% side-by-side columns\n- **List rows** — flex-row with fixed-width label + content div (agenda/schedule slots) → single text-editor with absolute-positioned label (reliable cross-browser)\n- **Header navs** — `<header>` / `<nav>` with logo + links + CTA → dedicated flex-row layout with icon-list + button\n- **Styled wrappers** — any `<div>` with gradient / solid non-white bg / border-radius preserved as inner container\n\n### CSS features supported\n\n- Cascade with specificity sorting\n- Inheritance of `color`, `font-*`, `text-align`, `letter-spacing`, `text-transform`\n- Inline `style=\"\"` attributes (highest priority)\n- **CSS custom properties** (`var(--x)`) resolved from `:root` / `html` / `body` (with chain resolution)\n- Shorthand expansion: `padding`, `margin`, `border`, `border-radius`, `background`\n- Named color keywords (`white`, `black`, `red`, `purple`, …)\n- Linear gradients (parsed into Elementor gradient settings)\n\n### Responsive behavior\n\n- `flex-wrap: wrap` → propagates to tablet + mobile\n- Row flex containers default to `flex_direction_mobile: column` (stack below 768px)\n- Typography globals respected at all breakpoints\n- Tested at desktop / tablet / mobile\n\n---\n\n## Limitations\n\n**Will not work well:**\n- JS-rendered pages (Next.js hydration, React CSR, etc.) — use the optional `--url` mode with Playwright if needed\n- Media queries with `@media (min-width: …)` — currently no extraction\n- Grid tracks beyond `repeat(N, 1fr)` — e.g. `grid-template-columns: 2fr 1fr` collapses to \"N columns\"\n- CSS animations, transitions, `transform`\n- Pseudo-elements (`::before`, `::after`, `:hover`, `:focus`)\n- `backdrop-filter`, `clip-path`, complex `mask` effects\n\n**Elementor-specific gotchas:**\n- Elementor's lazy-load experiment hides `background-image` on containers below the fold until scroll. If your imported page shows missing section backgrounds, disable it:\n  ```bash\n  wp eval 'update_option(\"elementor_experiment-e_lazyload\", \"inactive\");' --allow-root\n  ```\n- `image_custom_dimension` only works with files already in the WP media library. Remote URLs fall back to natural image size.\n- System colors (`primary`, `secondary`, `text`, `accent`) are **site-shared**. `html2elementor` only uses `custom_colors` with page-unique hashed IDs to prevent page A's palette from overwriting page B's.\n\n---\n\n## Architecture\n\n```\nhtml2elementor/\n├── __init__.py      # Public API: convert(html) → {layout, kit}\n├── cli.py           # `python3 -m html2elementor input.html -o out.json`\n├── parser.py        # HTML → tree of {tag, classes, text, styles, children}\n├── resolver.py      # CSS cascade: selector matching + specificity + var() sub\n├── sections.py      # Top-level section detection\n├── widgets.py       # DOM → widget specs (heading, button, image, …)\n├── containers.py    # Section → flex container settings\n├── styles.py        # CSS parsing helpers (padding, radius, shadow, typography)\n├── colors.py        # hex/rgb/named-color → #hex, darken(), lighten()\n├── globals.py       # Extract site-wide colors + typography into kit.json\n├── hover.py         # Hover state generation for buttons / icon-boxes\n├── media.py         # Optional image upload helpers\n├── builder.py       # Assemble final _elementor_data JSON (IDs, nesting)\n└── verify.py        # Diff output vs source (colors, sizes, spacing)\n```\n\nPipeline:\n```\nHTML string\n  ↓ parser.parse_html (BeautifulSoup + inline styles)\n  ↓ resolver.resolve_all (cascade + var() + inheritance)\n  ↓ sections.detect_sections\n  ↓ containers.map_section (per section)\n    ↓ widgets._walk (recursive DOM traversal)\n      ↓ emit widgets with Elementor settings\n  ↓ globals.consolidate (extract kit + rewrite references)\n  ↓ builder.build_layout (assign IDs, nest elements)\n  ↓ JSON\n```\n\n---\n\n## Testing\n\n10 test pages covering common modern landing patterns:\n\n| Test | Pattern |\n|------|---------|\n| `portfolio.html` | Minimalist creative portfolio |\n| `education.html` | Edtech / courses |\n| `pricing.html` | SaaS pricing plans |\n| `analytics.html` | Complex SaaS landing (10 sections) |\n| `conference.html` | Event with agenda + speakers + sponsors |\n| `studio.html` | Agency with left-aligned hero |\n| `blog.html` | Magazine with split hero + image cards |\n| `app.html` | Newsletter form with input+button |\n| `team.html` | Team photos + values grid |\n| `ai-saas.html` | AI-generated style (CSS vars, Tailwind-ish classes) |\n\nRun all tests:\n```bash\nfor t in portfolio education pricing analytics conference studio blog app team ai-saas; do\n  python3 -m html2elementor html2elementor/tests/$t.html -o /tmp/out.json\n  python3 -m html2elementor.verify html2elementor/tests/$t.html /tmp/out.json\ndone\n```\n\nCurrent status: **95% visual match on AI-generated HTML** after 5 iterations of a screenshot-diff loop.\n\n---\n\n## Design principles\n\n- **Local, deterministic, free forever.** No API keys, no services, no \"AI credits.\"\n- **Prefer visual fidelity over semantic fidelity.** A text-editor with inline HTML that renders correctly beats a semantically-pure widget that looks wrong.\n- **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.\n- **Fail loud, not silent.** `verify.py` catches missing colors, wrong sizes, and structural drift before you import.\n\n---\n\n## Roadmap\n\n- [ ] Extract `@media` queries into tablet/mobile overrides\n- [ ] Support `grid-template-columns: 2fr 1fr` (non-uniform widths)\n- [ ] `:hover` state extraction for buttons\n- [ ] Image upload into WP media library via REST API\n- [ ] Elementor template kit format (`.zip` instead of raw JSON)\n- [ ] VS Code extension (paste HTML → preview Elementor)\n\n---\n\n## Contributing\n\nPRs welcome. The shortest path to a useful contribution:\n\n1. Pick a landing page (your own, or a competitor's exported HTML).\n2. Run `python3 -m html2elementor your.html -o out.json`.\n3. Import, screenshot, compare.\n4. If something looks wrong, find the fix in `widgets.py` or `resolver.py`, add a test file to `tests/`, send a PR.\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md) for dev setup.\n\n---\n\n## License\n\nMIT. Use it, fork it, resell it in your SaaS, doesn't matter — just don't pretend you wrote it.\n\n---\n\n## Author\n\nBuilt by [@dudaster](https://github.com/dudaster). If this saves you money on a commercial license, consider starring the repo.\n"}