~10 min read · Engineering
There are roughly three honest ways a visual editing tool can relate to your codebase.
The first is export and fork: you design in the tool, it generates code, you paste it into your project, and from that moment forward the tool and your repo are strangers. This is how most AI code generators (v0, Bolt, Lovable) work — and for greenfield components, it's fine. You generate, you customize, you ship.
The second is SDK ownership: the tool becomes a runtime dependency. Your components are stored in a proprietary model, rendered via the tool's SDK, and deployed through the tool's infrastructure. Leave the tool, and you're migrating a schema, not just moving files. This is roughly how Plasmic and Builder.io work — and again, for the right use case (marketing pages, designer-editable CMS content), it makes sense.
The third — the one we bet CrossUI Studio on — doesn't have a clean industry name yet. We call it Symmetric Collaborative Development (SCD): your React source code is the single source of truth, and the visual canvas is a peer editor of that source. Every visual change is written back to your file as a precise AST patch. No export step. No SDK. No lock-in. Stop using the tool tomorrow and nothing breaks.
This post explains why we think the third model is the right one for senior engineers maintaining real codebases — and what it actually takes to build it.
TL;DR. If your existing repo is the source of truth and you want a visual layer over it, the editor has to write back through the AST — atomically, one property at a time — or it will lose against your code reviewer. Everything else is a different product for a different team.
The git diff test
Here's the simplest way to evaluate any visual editor. Three steps:
- Open the tool with a real MUI component already in your codebase.
- Change
variant="text"tovariant="contained"on a button. - Run
git diff.
Every "design-to-code" tool I evaluated over the last five years failed this test, in one of two ways. Either the file was re-emitted entirely — comments stripped, formatting normalized, imports reshuffled, hand-tuned useMemo blocks subtly rearranged — or the tool refused to touch existing files at all and emitted a parallel "design document" we then had to glue back into the project.
Neither was acceptable for a team that takes diff hygiene seriously. We rejected three tools in a row on this basis.
The third time, I decided the category itself was wrong.
Why "re-emit the file" is such a hard habit to break
If you read how most visual editors are architected, the root assumption is: the tool owns the rendering model. The tool maintains its own representation of the component tree — either in a proprietary schema or in an in-memory tree — and "saving" means serializing that representation back to JSX.
Serialization is lossy by nature. The tool's model doesn't know about your comment. It doesn't know why you named a variable isSaving. It doesn't know that your /* Main content area */ block comment is something a human will read in code review. So when it writes back, it writes what it knows — the component structure, the props, the styles — and silently discards everything it doesn't.
The result is a visually identical file that is a conceptually different file. Same output, different authorship. If you're a senior engineer who cares about your codebase, that's enough reason to never use the tool again.
The only way to avoid this is to not use serialization at all. Which means working at the AST level from the start — and the AST is hard.
What AST-level sync actually requires
Abstract Syntax Trees are how JavaScript parsers represent code internally. Every identifier, every JSX attribute, every function call has a position in the tree. If you want to change variant="outlined" to variant="contained", you don't re-serialize the whole file — you find the JSXAttribute node whose name is variant, update its StringLiteral value, and write back only the characters that changed.
This is what CrossUI Studio's engine does. The practical consequences:
Formatting is preserved. We don't pass the file through Prettier on every visual edit (though we do run Prettier on deliberate code saves). The unchanged bytes don't move.
Comments survive. AST nodes have attached comment ranges. We track them and leave them where they are.
Business logic is untouched. The engine identifies which AST node corresponds to the visual change and patches only that node. Your custom hooks, your useCallback wrappers, your conditional rendering logic above and below the component — we don't touch any of it.
The undo stack is unified. Whether you typed in the code editor, dragged a component on the canvas, or changed a value in the inspector, every operation is a reversible AST mutation. Ctrl+Z rolls back the code and the canvas in sync.
The visible result for the engineer:
{/* keep until the new spec lands — Jordan, Apr 14 */}
const total = useMemo(() => sum(order.items), [order])
return (
<Card sx={{ p: 2 }}>
- <Button variant="text" onClick={handle}>Pay</Button>
+ <Button variant="contained" onClick={handle}>Pay</Button>
</Card>
)
One line removed, one line added. The comment, the useMemo, the surrounding JSX — byte-identical to before. The diff that lands in code review is exactly the change the engineer would have typed.
The depth problem: why .map is the real test
Even among tools that do some form of code-aware editing, almost all share the same limitation: they can only operate on the outermost JSX block.
Consider this common pattern:
{items.map((item) => (
<Card key={item.id} elevation={item.featured ? 3 : 1}>
<CardContent>
<Typography variant="h6">{item.title}</Typography>
</CardContent>
</Card>
))}
A typical visual editor sees the map expression. It cannot edit the Card inside it visually — because to render a canvas for the Card, it needs to know what item is, and item only exists at runtime inside the callback.
CrossUI's Layer Drill-down Engine solves this with a Test Data Injector. When you drill into a .map callback, Studio asks: "what's the shape of item?" You provide a mock value — or it infers one from surrounding usage — and Studio renders an isolated canvas for the inner block, with item injected as a real prop. You edit the Card visually. The changes are written back to the callback body as precise AST patches. The outer items.map(...) expression is untouched.
The same mechanism works for ternary branches (cond ? <A/> : <B/>) and children render slots. You can drill any depth your component tree requires.
This is the feature that separates a toy from a tool you'd actually use on a production codebase.
MUI isn't generic React
One more thing the "re-emit" tools get wrong: they treat all React components the same.
MUI components have a richer prop surface than native HTML. variant, size, color, elevation, sx — these aren't just className strings. They're typed, enumerated, and in some cases highly structured. The sx prop accepts a deep object with pseudo-class keys, state-based keys, and responsive breakpoint objects like { xs: 'small', md: 'large' }. Generic React visual editors render an untyped text field for sx and call it done.
CrossUI Studio ships a dedicated MUI panel with:
- Enumeration completions for every MUI prop value (correct values, no guessing).
-
sxtree editor — nested objects, pseudo-classes, and media queries in a structured visual tree. -
Responsive property promotion — click the lightning icon next to any MUI system prop and it splits into a breakpoint object. Change the value at LG and the code becomes
size={{ xs: 'small', lg: 'medium' }}automatically. -
Design Token access — your custom
createTheme()tokens surface in the inspector.
These aren't features you can retrofit onto a generic visual editor. They require knowing what MUI is, how it works, and where the edge cases are.
What we gave up
The SCD model has real costs. It's why most tools don't do it. We owe an honest accounting:
- We don't work on every React codebase. If your components are deeply entangled with SSR edge cases, or your bundler produces non-standard output, the AST layer may not parse cleanly. We document the boundaries instead of pretending they don't exist.
- Focus is our superpower. No Vue, no Svelte, no Angular — not on the roadmap. We'd rather be the best possible tool for one ecosystem than a mediocre tool for five. We are dedicated exclusively to the React ecosystem, deeply supporting leading design systems like MUI and shadcn/ui, with expandable support for Joy UI, Ant Design, and beyond.
- We don't generate apps. If you want to prompt your way to a working dashboard, use v0 or Bolt. They're genuinely good at that. We're for engineers who already have a codebase and want a canvas that respects it.
- We don't host your code. No CrossUI-side storage. Git operations are direct browser-to-provider. That's a feature, but it also means we can't offer cloud workspaces, team collaboration on a shared document, or any of the conveniences a hosted model enables.
- We evolve alongside the ecosystem. We closely follow major updates of our supported component libraries. While we prioritize empowering teams on the latest versions with robust, modern alignment, we continually evaluate expanding our backwards compatibility. If you are locked into legacy framework versions, we might not be the right tool for you — yet.
The decision
Three honest recommendations, no false modesty:
| If you're… | …use |
|---|---|
| Building a marketing site that designers need to edit | Plasmic |
| Prototyping from scratch and want AI generation speed | v0 or Bolt |
| Handing Figma files off to engineers, once | Locofy or Anima |
| Maintaining a React + MUI codebase, care about your git history, want a visual canvas that treats your code as the source of truth | CrossUI Studio |
The Playground is free, works immediately, and doesn't require connecting a repo. Paste any React snippet and see it on the canvas.
About CrossUI Studio — A visual IDE for React & MUI. Code and canvas stay in two-way sync on the same AST: edit code and the canvas updates live; click an element on the canvas, edit its props visually, and the code changes with a surgical one-line diff. No build, no localhost — it runs in the browser, works on your real Git repo or a local folder, with no vendor lock-in.

Top comments (0)