"Figma to Elementor": the search keeps coming back, and the expected answer is a button. Import, convert, publish. I don't use any. My latest campaign, a thirteen-section page 11,109 pixels tall, went from Figma to the site between August 28 and September 15, 2026, with Claude Code and a handful of MCPs. Here is the method, the tools, and the question to ask before the first line of code.
The question that decides everything: who will edit the page?
Before opening Figma, I ask the project one question. Not "how do I reproduce this mockup?". Rather: "who will touch this page after me, and to do what?" Two possible answers, two architectures.
| Criterion | Native Elementor widgets | PHP template + ACF inside an Elementor shell |
|---|---|---|
| Who edits | An editor who composes their own pages | Someone who changes text and images |
| Freedom | Insert, duplicate, reorder blocks | Fill in fields, the layout never moves |
| Pixel accuracy | Good, bounded by Elementor's controls | Total: the CSS is hand-written |
| Repeated lists | Widgets repeated one by one | ACF repeater, one PHP loop |
| Writing tool | EMCP Tools | PHP, ACF, WP-CLI |
| My case | The magazine site (mockup pending) | The awareness campaign |
The campaign was an awareness page for a public health association. A long page, fixed content, and on the client side one person who changes text and images, never the structure. The PHP template was the obvious fit. A technical argument settled it: Elementor's Dynamic Tags cannot loop over an ACF repeater. And the mockup was full of them: lists, cards, testimonials.
The magazine site, on the other hand, will be composed by editors, without a line of code. They insert blocks, duplicate them, reorder them. There, only native widgets hold up. Same Figma in, opposite output.
Reading the mockup: two Figma MCPs, two roles
Claude Code can't see Figma. It needs a bridge. I use two, and neither replaces the other.
| Criterion | Official Figma MCP | claude-talk-to-figma plugin |
|---|---|---|
| Connection | Remote API, stateless | WebSocket to Figma Desktop |
| Parallelism | Several calls at once | One shared channel, one call at a time |
| Strength | Weights per text segment, source images | Exact colors, coordinates, full text inventory |
| Tools used | get_design_context, get_metadata, download_assets, get_screenshot | get_node_info, get_styled_text_segments, scan_text_nodes, export_node_as_image |
The official connector: structure and images
get_design_context returns React with Tailwind classes. For a WordPress theme, that code is useless as is: I only keep the structure, the texts, the colors and the spacing. Its real value lies elsewhere. It gives the weight of every text segment. On one paragraph, it showed at once that two proper names were Bold and everything else SemiBold, where the plugin would have required a full survey.
download_assets delivers the source images, not just the render: a 3,452 x 1,786 pixel file where the node export is 812 x 457. Figma's crop can be rebuilt from the original. A detail that matters: the URLs expire within minutes. Download right away.
The plugin: exact measurements
get_node_info gives a node's colors, absolute coordinates and exact characters. scan_text_nodes is the only way to find a text when you don't know where it lives: 87 texts on the French frame, a 58 KB result. I have a script read it, not the model. The plugin's only flaw: one shared WebSocket channel, one call at a time, frequent timeouts. When it stalls, I fall back on the official connector.
Three reading traps
- IDs change. Three sets of IDs since the start of the project, one per re-paste of the mockup. I find nodes by name or by text, never by an ID written down the week before.
- Big results overflow.
get_metadataon the whole page weighs 311,000 characters. The result goes to a file, a Python script extracts the inventory, and I only callget_design_contexton the nodes that matter. - A text's box is not the element's box. A list item measured 448.16 px in Figma: the width of the text alone. With the bullet indent, the right value was 475.4. One word wrapped, and a single pixel row gave it away.
Building: one section at a time, no design initiative
One rule opens every brief: no design initiative. Everything comes from Figma. Whatever is missing, a photo, a link, an absent text, becomes clearly flagged placeholder content and an open question. Never a text that could pass for final.
On the WordPress side, the Elementor page is just a shell. It holds a single widget, the [prevention_page] shortcode, which assembles the sections in a fixed order. ACF stores the content, and each section has its own Show/Hide toggle.
Each section then follows the same loop:
Figma (1440 px frame)
│ official MCP : structure, weights, source images
│ plugin : colors, coordinates, texts
â–Ľ
Section plan (.md) measurements, traps, open questions
â–Ľ
section-*.php + ACF fields + CSS block
â–Ľ
Idempotent content script wp eval-file script.php <post_id>
â–Ľ
Headless capture at 1440 px → pixel rows against the Figma render
â–Ľ
Gap ≤ 4 px? no: fix yes: commit
Claude Code wrote the PHP, the CSS and the scripts. The trade-offs, the plan reviews and every visual sign-off stayed mine. The content script is idempotent: run it ten times, get the same result ten times, and it flags any content that is still a placeholder.
Measure, don't eyeball
The eye lies. A title set in Anton renders in a 74.4 px box where Figma counts 79. Aligning the boxes gives the wrong answer. Aligning the ink gives the right one.
So I compare the Figma render and a capture of the site at 1440 px with the same Python script (PIL, numpy): a color mask, contiguous pixel rows grouped together, and you get the top of every line and the extent of every box. Tolerance: 4 px on lines and boxes, 4 px on section height. Any larger gap is logged with its cause.
The capture comes from a windowless Chrome, driven through the DevTools protocol. One constraint showed up along the way: comparing pixels requires the same browser. A baseline taken in one Chrome version against captures from another differed everywhere, because of font rendering.
Living with Elementor without letting it take over
A PHP template inside an Elementor site still inherits from Elementor. Three traps, all of them hit:
- The kit wins the conflicts. The global kit forces font and color on every heading with a 0-1-1 specificity selector. A plain class rule (0-1-0) loses. The fix: prefix with the page's root container (0-2-0). No
!important. - The old CSS stays loaded. Elementor loads
elementor-post-<ID>.cssbased on the page ID, not on how the page renders: 46 declarations from an old layout were still active. A targetedwp_dequeue_styleremoves them without touching the header or footer. - Fonts don't follow. Elementor serves Google Fonts locally, page by page. A PHP template no longer benefits from that: the theme self-hosts its own.
Writing into Elementor: which MCP?
On the magazine site, native widgets are the way, and writing goes through an MCP. I compared what exists today by querying each server, not by reading its landing page. Measured on September 25, 2026, on Elementor 4.2.0 and Elementor Pro 4.1.2.
| Criterion | EMCP Tools | Official Elementor MCP | MCP Adapter | WP-CLI |
|---|---|---|---|---|
| Nature | Third-party plugin | Module built into Elementor | WordPress.org plugin | Command line |
| Tools exposed | 137 | 5 | None of its own | Everything, no guardrails |
| Classic widgets | Yes | No, atomic elements only | Not applicable | Yes, as raw JSON |
| Activation | Install the plugin | Hidden experimental setting | Install the plugin | Already there |
| My use | Main tool | Watch list | Foundation for the first two | Fallback |
EMCP Tools: the workhorse
137 active tools in version 3.17.1: classic and atomic widgets, containers, global classes, variables, templates, ACF fields, media, history with rollback. The 34 risky tools (file writes, SQL writes, plugins, users) are disabled by default. You enable them one at a time, never all at once.
I was still on 3.0.0. Updating to 3.17.1 wasn't a nicety: in between came fixes for data loss. update-page-settings replaced the whole settings object, so a one-key tweak on the kit wiped custom colors and typography. Third-party widgets silently vanished on save. Plugin and database backed up before, checked after: 51 Elementor pages, zero invalid JSON.
A habit worth keeping: re-read after every creation. During validation, add-container accepted css_classes without error and never saved it. get-element-settings shows what is actually stored.
The official MCP: five tools, for now
Elementor's announcement promises full pages, the design system, theme parts, custom widgets. In Elementor 4.2.0's code, the module already exists, behind a hidden experimental setting that is off by default:
wp option update elementor_experiment-e_wp_abilities_api active
Once enabled, it exposes five tools: list-pages, get-page-structure, update-page-settings, create-page, get-globals. It only targets V4 atomic elements. The magazine site has 51 Elementor pages, and only 11 contain atomic elements. The announced capabilities also require Angie, Elementor's agent, which I haven't installed. I keep it enabled to follow how it evolves. I explained elsewhere why V4 isn't my ground yet.
The MCP Adapter: why both coexist
EMCP and Elementor's module both register with the same WordPress MCP Adapter. Each gets its own route, no conflict: 137 and 5 tools answer side by side. One condition: EMCP 3.17.1 or later, which fixes an OAuth discovery bug where it answered in place of a second MCP plugin.
{
"mcpServers": {
"emcp-tools": {
"type": "http",
"url": "http://site.test/wp-json/mcp/emcp-tools-server",
"headers": { "Authorization": "Basic <base64(user:application-password)>" }
},
"elementor-official": {
"type": "http",
"url": "http://site.test/wp-json/elementor/mcp",
"headers": { "Authorization": "Basic <base64(user:application-password)>" }
}
}
}
WP-CLI: the fallback
Without an MCP, you write the _elementor_data JSON directly, the way Elementor does it. That's how I changed the mobile header on the campaign's site:
update_metadata('post', $id, '_elementor_data', wp_slash(wp_json_encode($data)));
delete_post_meta($id, '_elementor_css');
delete_post_meta($id, '_elementor_element_cache');
\Elementor\Plugin::$instance->files_manager->clear_cache();
It works, with no validation at all. Four rules learned on sites without an MCP:
- Back up with
mysql -N -r- without-r, the client doubles the backslashes and the JSON becomes invalid. _elementor_page_settingsis serialized PHP - read it as hex, thenupdate_post_meta(), or it gets corrupted.- A page created by script needs four metas -
_elementor_edit_mode,_elementor_template_type,_elementor_versionand the page template. Without them, the page renders empty, with no error. - URLs are stored escaped - a search-replace must also look for
https:\/\/.
The magazine site: the method, before the mockup
The magazine site's mockup hasn't arrived yet. The site runs on a provisional design system: Elementor global colors and typography, CSS variables. Here is the order I will follow, taken from the campaign and adapted to native widgets:
- Inventory the mockup with
get_metadata, parsed by script, before any expensive call. - Carry colors and typography into Elementor's global settings, never widget by widget.
- Build the blocks one by one on a workshop page, through EMCP.
- Re-read every creation with
get-element-settings. - Measure at 1440 px against the Figma render, same script, same tolerance.
- Sign off each block myself before turning it into a reusable template with
save-as-template.
The difference from the campaign fits in one line: the end result is no longer a page, it's a library of blocks that editors assemble without me.
Results
| Metric | Value |
|---|---|
| Campaign build | August 28 to September 15, 2026 |
| Commits over the period | 200 |
| Mockup | 1,440 x 11,109 px, 13 sections |
| Texts inventoried | 87 in French, 96 in Dutch |
| PHP section files (full template) | 21 |
| Template CSS | 4,913 lines |
| Assets exported from Figma | 28 |
| Measurement tolerance | 4 px |
| EMCP Tools 3.17.1 tools | 137 |
| Official MCP tools (Elementor 4.2.0) | 5 |
Limits
- The PHP template locks the client in. They change text, not structure. A new section means code. A deliberate choice, not a hidden flaw.
- The official MCP is young. Five tools, atomic elements only, behind a hidden setting. Worth re-checking with every release.
- EMCP is a third-party plugin. Seventeen minor releases since 3.0.0. Every update calls for a backup and a read of the changelog.
- Measuring doesn't judge taste. A zero gap says the page is faithful, not that it's good. Visual sign-off stays human.
The takeaway
- The first question isn't technical. Who will edit the page decides the architecture, before Figma is even open.
- Reading and writing are two jobs. Two MCPs to read the mockup, one MCP to write into Elementor, WP-CLI when neither is enough.
- Query the server, not the landing page. 137 tools on one side, 5 on the other: no announcement said so.
- Measuring beats eyeballing. Pixel rows, a tolerance, and every gap explained.
- It's not a magic button. It's a method, tools and the discipline to check. The magazine site will start from there.
Resources
The two MCP servers for Elementor, the WordPress foundation that lets them coexist, and the Figma plugin used for exact measurements.
EMCP Tools · MCP Adapter · Elementor MCP · claude-talk-to-figmaThe full tutorial, on request
This article sums up the method. I can turn it into a full step-by-step tutorial: the pixel measurement script, the section plan template, the Figma and Elementor MCP setup, and every trap logged along the way. Moving from Figma to Elementor and want that tutorial, or a hand with your project? Write to me.
Get in touch