CrossUI Studio

The IDE for React & MUI — AI writes the logic, you tune it.

Visit Website
August 31, 2026 Drill down. All the way — even when the real code is five files away

You click a card on the dashboard. Looks like nothing — one component, right there in the file you're already looking at.

It isn't. It's one of several produced by a .map(). It's the branch of a conditional that happens to be on screen — the other branch is one state change away, and invisible right now. What actually draws it is a wrapper, with the real component a layer or two underneath. And that real component lives in a different file, reached through a re-export. Four walls between the pixel you clicked and the code that made it: a loop, an invisible branch, a wrapper, a re-export.

Most tools stop at the first wall. Land you in the wrong place, or just the nearest place, and call it done. That's the actual pain: you click, you get somewhere, but not there. You still have to go hunting.

We built drill-down to keep going. Through the .map(), through the wrapper, through the re-export, all the way to the file where the thing is actually defined — and back up again, one layer at a time, whenever you want.

Why stopping at the first wall is the default failure mode

The screen you're looking at is a flat tree of pixels. The source that produced it is a nested tree of expressions, conditionals, and loops, spread across however many files someone decided to spread it across. These two trees don't line up file-for-file, wall-for-wall.

So a click can't just be "find the matching thing." It has to be a path — DOM node, then the JSX expression that drew it, then the component call, then the component's actual definition, then the file that definition lives in, then the call site above that passed the props in. Stopping one layer too early is the single most common way a "click to find it" feature quietly fails.

Four walls that stop a shallow tool cold

The .map() wall. One line of JSX produces five cards. Click any of them, you land on that one line — we don't pretend to know "which iteration" you clicked, that's runtime data, not source structure, faking it just makes the tool unpredictable. What we do instead: land on the map expression, then let you drill one more level down, into the actual template structure sitting inside it. Two honest stops instead of a guess.

The invisible-branch wall. {isVip ? <GoldBadge /> : <SilverBadge />} — only one branch is on screen right now. Click into the one that isn't, and a shallow tool just says "nothing to show." We render that branch on its own instead, like flipping the switch for a second, so you actually see it.

The re-export wall. The component you clicked isn't defined where you are. It's imported, maybe through a barrel, maybe through two. A tool that stops here drops you at the import line and calls it a day. Drilling through means landing on the real definition, without losing track of what props the original call site was passing — otherwise you land in a strange file with no idea how you got there.

The wrapper wall. withAuth(withTheme(Card)). Click the rendered thing — is "this component" the innermost Card, or something a wrapper is doing? A shallow tool picks one and hopes. Drilling means you can move through every layer of wrapping, one click at a time, and see what each one is actually responsible for.

The rule that keeps the drill-down predictable

One rule, applied every time: land somewhere definite first, then give an explicit way to go deeper or pull back — never a silent guess.

First click: the smallest meaningful JSX unit closest to what you clicked. Not the outer wrapper, not the raw DOM leaf.

Click again, or use an explicit gesture, and you move up the chain — component call, then the call site that passed props in, then the file the component actually lives in. This is the same chain behind the breadcrumb trail from our earlier walkthroughs — here we're talking about the rule driving it, not the visual.

This rule only got solid after running it against a few hundred real templates. It wasn't right on a whiteboard the first time.

Drilling the other direction, from code into canvas

Cursor on a .map() expression: every one of the N rendered results lights up on canvas, not just the first.

Cursor on a HOC's definition: every rendered instance using that wrapper lights up — could be scattered across totally different screens.

Cursor on an inactive branch: canvas renders that branch on its own so you can actually see it, instead of a dead end.

The nastiest case we could find

shadcn-admin, the left sidebar — components/layout/nav-group.tsx. You click one nav item. On screen it's just a button.

It isn't one thing. It's one of N produced by items.map(...), and inside that map a three-way branch decides what the item even is: a plain <SidebarMenuLink> when it has no sub-items, a <SidebarMenuCollapsedDropdown> when the sidebar is collapsed, or a <SidebarMenuCollapsible> otherwise — so two of the three are off screen right now, in whatever state you're not currently in. The button you actually see is <SidebarMenuButton asChild>, a Radix Slot wrapper that merges its props onto its child, and its real definition isn't in this file at all — it's in @/components/ui/sidebar, behind a barrel export.

Click it. First stop: the closest JSX layer. Drill up: you're inside the items.map(...) that generates the whole menu. Flip to the branch that isn't showing — the collapsed-sidebar dropdown — and the canvas renders just that branch on its own, without actually collapsing anything. Peel the asChild Slot to see what it's merging onto. Jump to the real SidebarMenuButton: a different file, through the barrel.

Four walls — a .map(), an invisible branch, a Slot wrapper, a re-exported definition — one click, and you get through all of them. (Every name above is real: nav-group.tsx, SidebarMenuLink/`SidebarMenuCollapsible`/`SidebarMenuCollapsedDropdown`, and SidebarMenuButton from @/components/ui/sidebar.)

Why this is a real pain point, not a nice-to-have

Every "click to inspect" feature that exists eventually hits one of these four walls and just stops — usually silently, so you don't even realize you're in the wrong place until you've wasted five minutes editing the wrong file. That's the actual cost: not "this feature is missing," but "I trusted where it took me and it was wrong."

What this means for teaching React

Put a cursor inside a .map(), watch eight elements light up at once — that teaches list rendering better than any explanation of the key prop ever has, and it only works because the tool knows exactly how many things to light up.

HOCs and wrapped components are one of the hardest things for beginners to reason about — "what even is this component" doesn't have one right answer. Drilling up through the wrapper stack, one layer at a time, and seeing what each one is actually doing, beats defining "higher-order component" on a slide.

Being able to see both branches of a conditional side by side — what a VIP sees, what everyone else sees — without touching state or faking test data is something a lot of people never get straight even years into writing React.

What this means for reviewing AI's diffs

The common workflow now: AI hands you a diff, you review it. Real problem — if that diff touches one line inside a .map(), you genuinely can't tell from the text alone whether it changes every item in that list or just one special case. Layer ambiguity is a live blind spot in code review, and it's exactly the kind of thing that slips through when someone's skimming a PR at the end of the day.

With a real notion of layer, an AI-generated change can be labeled by what it actually touches — a shared component definition (hits every call site) versus a single call site's props (hits just this one). That distinction is the whole ballgame for judging whether a change is safe, and most diff views today just don't carry it.

Push it further — if an AI agent itself can sense which layer a change lands on, it avoids a common failure mode: thinking it edited one instance when it actually touched the shared definition, quietly breaking five other places that use that component.

Three questions for any tool claiming to handle this

  1. Click one of several elements generated by a .map() on the canvas — does the code correctly select the map expression that generated it? Can you drill one step further into the actual template? What happens with nested maps?

  2. Click a component wrapped in a HOC — can you climb up / down, one layer at a time, and see what each wrapper actually does?

  3. Put your cursor on a conditional branch that isn't currently rendering — does the canvas actually render that branch for you, or does it just apologize that there's nothing to show?

    Originally published at blog.crossui.com on August 30, 2026.

3 Comments

  1. 1
    The four “walls” make the problem much clearer than the feature list. Curious whether users are adopting CrossUI mainly because existing visual editors break on complex real-world React code, or because AI-generated code has made that layer ambiguity painful enough to create a new workflow altogether.
    1. 1

      A bit of both, but AI made it urgent. Traditional visual editors always choked on complex React patterns, but AI generation tipped the scale. AI makes writing code instant, but devs now need a way to visually drill down and edit that output without breaking the AST.

      1. 1
        That makes sense. The way AI changed the urgency around an existing editor problem is interesting to watch as you get more users on it.
July 13, 2026 Editing React components that never rendered

There are only two ways a tool can learn the shape of your React code. Which one you pick decides the single most important thing about the tool: whether it’s available in the exact moment you need it most.

Two ways to know a codebase

Runtime introspection watches a running app. React DevTools, profilers, error overlays — they read the live fiber tree after the app mounts. This is the richer of the two: they know real prop values, real state, what actually rendered. But all of that is conditional on one thing — the app has to successfully run first. The moment a render throws, they go dark. Nothing mounted, so there’s nothing to inspect. And a module that failed to initialize is invisible to them by definition.

Static AST analysis reads source. It parses each file into an abstract syntax tree and reads the structure directly — every JSX node, every import, every prop — without executing anything. It’s poorer in one sense: it can’t tell you what a variable held at 2:04pm, because nothing ran. But it has a property no runtime tool can match: it works on code that has never once rendered cleanly.

Most developer tooling is built on the first model. We built CrossUI Studio — a visual editor for React — on the second, on purpose. This post is about why that constraint turned out to be the whole point.

The moment tooling abandons you

Here’s the scenario that shaped the decision. A component renders blank. The stack trace points at the component, but the real failure is five imports down — a dynamic import that didn’t resolve, a peer dependency that got bumped, a circular reference that’s undefined on first access. The component is innocent; the thing it transitively pulls in is the culprit.

This is exactly when you most need to understand your code’s structure — which file imports what, where the break is, what the surrounding code looks like. And it’s exactly when every runtime tool has gone dark, because the app didn’t render. DevTools shows nothing. The profiler has no render to profile. The error overlay gives you a line number but not the shape of the code around it. They all need the app to run first, and the entire problem is that it won’t.

The one moment you most need to see your code’s structure is the one moment a runtime tool refuses to produce it.

A static AST tool has no such dependency. It parsed the files when you opened them; it can draw the whole import graph, point at the broken module, and show you the code around it — with the app fully crashed. A broken page isn’t a dead end. It’s the entry point.

Editing what you can’t run

The same property unlocks something stranger: editing a component that isn’t rendering, in a file you never opened.

Think about what a visual editor normally needs to render a <Card> that lives inside a .map(). To put that Card on a canvas, it has to know what item is - and item only exists at runtime, inside the loop, when real data flows through. A runtime-based editor simply can't render that Card in isolation; there's no live item to feed it. So most visual editors quietly refuse to go inside a .map() at all.

Working from the AST, the problem reframes. We don’t need a running loop; we need the node. Parse the file, find the JSX inside the map callback, and render that subtree in isolation. The one thing missing is the value of item - so we ask you for it (a small mock in a panel) rather than requiring the app to produce it. The Card renders, live, with real theming, standing on its own.

And because the unit of understanding is the AST node rather than the mounted component, drill-down doesn’t stop at the file boundary. A child component imported from another file is just another edge in the graph. Follow the import, parse that file, render its node — you’re editing a component defined somewhere you never opened, in an app that may never have rendered as a whole.

The cost of the constraint

None of this is free, and pretending otherwise would be dishonest. Static analysis buys unconditional availability by giving up runtime knowledge:

  • Fully dynamic imports are unresolvable. import('./pages/' + name) with a runtime-computed path can't be resolved statically. We mark the edge rather than guess.

  • We see structure, not values. The AST tells you AIPanel reaches TreeSitter; it can't tell you what the parser returned on a given render. Mock data stands in for real data; it doesn't replay it.

  • Exotic resolution needs config. Non-standard bundler aliases or monorepo path magic resolve best when we can read your tsconfig/jsconfig. Without it, some edges fall back to raw specifiers.

These are real boundaries. But notice none of them touch the core promise: the tool is there when you’re stuck, because it never needed your code to run in the first place.

Why this is one idea, not many

The crash-surviving dependency graph, the edit-inside-a-map drill-down, the cross-file navigation, the one-line diffs — from the outside they look like separate features. They’re the same decision seen from different angles: treat parsed source as the source of truth, and make every surface a view over that AST.

The canvas is a view over the AST. The prop inspector is a view over the AST. The dependency graph is a view over the AST across files. None of them depend on a successful render, because none of them are built on the runtime. That’s a harder thing to build than hooking the fiber tree — and it’s exactly what keeps the tooling alive in the moments the runtime, and everything built on it, goes dark.

Try it — open a project, break a deep import on purpose, and watch the structure still render.

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.

Try it free → studio.crossui.com


Originally published at https://blog.crossui.com on July 13, 2026.

Comment

July 6, 2026 https://blog.crossui.com/2026/07/layer-drill-down-edit-any-depth

Edit inside a .map(), a ternary, even a component in another file

Here’s the wall almost every visual editor hits. You can edit the outer JSX just fine. But the thing you actually need to change is a Card inside.map(), and the tool goes quiet - because to render that Card, it needs to know what item is, and item only exists at runtime, inside the callback.

So you drop back to hand-editing. Which is fine, except the whole reason you opened a visual editor was to not do that.

CrossUI Studio’s Layer Drill-down Engine is built for exactly this. You can isolate and edit any node at any depth — including the ones that live behind a runtime boundary. And drill-down doesn’t stop at the file edge: Ctrl+click a child component and Studio follows it into the file where it’s defined, keeping the canvas live the whole way down.

The depth problem, concretely

Take a list that every React app has some version of:

{items.map((item) => (
<Card key={item.id} elevation={item.featured ? 3 : 1}>
<CardContent>
<Typography variant="h6">{item.title}</Typography>
<Chip label={item.tag} size="small" />
</CardContent>
</Card>
))}

You want to visually tweak that Card - bump the padding, change the Chip size, adjust the Typography variant. But a normal visual editor can only see the items.map(...) expression. It can't render the inside, because item is undefined until the loop runs. Same story for a ternary ( cond ? <A/> : <B/>) and for children passed into a slot.

That runtime boundary is where most tools stop. Studio treats it as just another layer to step into.

Step 1 — Drill into the iteration

There are three ways to drill into a node, and they work whether the target is in the current file or in another one:

  • Ctrl+click on the canvas or in the component tree. Hover a drillable sub-component or expression and an action prompt appears; hold Ctrl to highlight the target with a mask, then click to step into it. Or just click the corresponding node in the left component tree.

  • JSX block selector / breadcrumb. Use the block selector or breadcrumb above the editor to see the hierarchy and jump to any level — handy when the tree is dense and you’d rather pick than click through.

  • Cursor focus (in-file). In the code editor, place your cursor inside a JSX block and Studio promotes it to the canvas focus. Editor and canvas point at the same AST, so moving in one moves the other.

Drill into the .map() and Studio renders an isolated canvas for the inner block - one iteration of your list, standing on its own.

Ctrl+click the .map() to drill into a single list itemCtrl+click the .map() to drill into a single list item

In the shot above, Ctrl+click on the .map() drills into ProjectBoard['map-0-item'] - the first item of the list, isolated on the canvas while the full items.map(...) stays put in the code.

Supply the missing context: the Test Data Injector

An isolated list item has a problem: it lost the context its parent gave it. item is undefined now - there's no loop feeding it. So Studio gives you a Test Data Injector: a panel where you supply temporary mock data (JSON, strings, booleans) for any undefined props or variables at the current level. Fill in item and the isolated Card renders realistically, without the full app around it.

One thing that saves guesswork: hover any unassigned variable and the code editor highlights its exact location in the source, so you can see how item is used before you decide what to mock.

Now the Card is live. Not a screenshot - the actual component, rendered with real MUI theming.

Step 2 — Edit the inner node

With the inner block isolated, editing is the normal Studio loop. Click the Chip, change size from small to medium. Select the Card, promote its padding to a responsive { xs: 2, md: 3 }. Each change writes back into the callback body as a precise AST patch:

{items.map((item) => (
- <Card key={item.id} elevation={item.featured ? 3 : 1}>
+ <Card key={item.id} elevation={item.featured ? 3 : 1} sx={{ p: { xs: 2, md: 3 } }}>
<CardContent>
<Typography variant="h6">{item.title}</Typography>
- <Chip label={item.tag} size="small" />
+ <Chip label={item.tag} size="medium" />
</CardContent>
</Card> ))}

The .map() wrapper, the key, the ternary on elevation - all untouched. Your mock data never touches the file; it lived only in the drill-down session. What lands in the diff is exactly the two properties you changed.

The same mechanism works for conditional rendering (drill into the true or false side of a ternary, or a && branch, and edit it in isolation), for controlled sub-components that depend on external props or state, and for render-slot children. Any depth your tree actually has, you can reach.

Step 3 — Cross the file boundary

Real components don’t live in one file — a page composes children that are imported from all over src/. A file-bound editor stops when it hits one of those imported children: you have to go find the file yourself, open it, and lose the canvas.

Studio doesn’t stop there. Ctrl+click a child component and Studio drills straight into the file where it’s defined, and keeps rendering it live on the canvas.

Ctrl+click a child component to drill into the file where it’s definedCtrl+click a child component to drill into the file where it’s defined

In the shot above, the active file is ShadcnDashboard.jsx. Its component tree lists the children it composes - KpiCard, RevenueChart, OrdersTable, Sidebar, Header, and so on. Sidebar isn't defined in this file; it's imported from ./components/Sidebar.jsx (line 16). Ctrl+click it, and Studio follows the import, opens the real definition, and drops you onto its canvas - the dashboard's sidebar, rendered live - with no manual file hunting and no losing your place. From there you can keep drilling: into that file's own children, into a .map() inside it, into a component it imports from somewhere else.

The same three drill-down methods from Step 1 apply here — Ctrl+click and the JSX block selector both cross file boundaries (cursor focus is the one that’s in-file only, since it needs the code open in front of you). The effect is that the component tree stops being one-file-at-a-time. You navigate your UI the way it’s actually structured — as a graph of components across files — while the canvas stays live at every hop.

Why it can do this

Same reason the rest of Studio works the way it does: everything is a view over the AST, not over a running app.

Drilling into a .map() is an AST operation - Studio finds the callback body node and renders it with an injected scope. Following a child component across files is also an AST operation - it resolves the import, parses the target file, locates the exported component, and renders that node. The runtime never has to succeed for any of this; Studio is reading structure, not observing execution. That's why it can isolate a node five levels deep, in a file you haven't opened, whether or not the full app renders.

If you want the architecture behind that, the dependency graph post covers how static AST parsing survives things a runtime tool can’t.

Try it

The fastest way to feel this is on a real project with real nesting — a dashboard with a list of cards, a page that composes imported sections.

  1. Open a project (Playground, Git repo, or a local folder).

  2. Find a .map() or a ternary, drill in, use the Test Data Injector to mock the missing variable, and edit the inner node.

  3. Find an imported child component in the tree, Ctrl+click it, and watch Studio follow it into its own file.

Guest mode, no account: studio.crossui.com

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.

Try it free → studio.crossui.com


Originally published at https://blog.crossui.com on July 6, 2026.

Comment

June 23, 2026 Why we rejected the export-and-fork model for visual UI editing

Why we rejected the export-and-fork model for visual UI editing

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.

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:

  1. Open the tool with a real MUI component already in your codebase.

  2. Change variant="text" to variant="contained" on a button.

  3. 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.

https://www.youtube.com/watch?v=TOiYf7aN4yo

AST Surgical update

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).

  • sx tree 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:

Building a marketing site that designers need to edit

Plasmic

Prototyping from scratch and want AI generation speed

Handing Figma files off to engineers, once

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.

Try it free → studio.crossui.com


Originally published at https://blog.crossui.com on May 17, 2026.

Comment

June 22, 2026 Don't just find the broken import. Bypass it.

Dependency Graph — zero build, survives a crash, injects a fix.

A dependency five levels down throws, and your whole canvas goes white. CrossUI Studio’s Dependency Graph still draws — zero build — points you straight at the blinking node, and then lets you inject a mock for the broken module to bring the page back. No file edit. No rebuild. That’s the v0.9.5 story.

The Dependency Graph is built from static AST parsing, not a build — open a file and the import tree renders instantly, even when the canvas has completely crashed. The failing module blinks with its exact line/column. Then the part that’s actually new: from the inspector you can set an interceptor or override to swap the broken dependency for a mock, hit Apply Dependency Injection, and the render comes back — rules auto-persist per entry file.

The blank-screen problem, five levels deep

A component blows up, but the component is never the culprit. Here’s a real one. The canvas throws inside AIPanel.jsx:

Error (Canvas render - runtime)
Parser engine initialization aborted.
in Studio/src/components/panels/AIPanel.jsx

at initializeParser (Engine/src/TreeSitter.js:32:11)
at async Engine/src/TreeSitter.js:36:31

But AIPanel isn't where it broke. The real failure is in TreeSitter.js, line 32 - five imports down the chain:

AIPanel.jsx → AIJsxResponseReconciler.js
→ JSXParserEngine.js
→ CodeFormatter.js
→ TreeSitter.js ← throws here

Normally this is where the afternoon disappears: rebuild to reproduce, scatter console.logs down the chain, guess which import actually threw. And the tools built to help have all just gone dark - React DevTools shows nothing (no component mounted), the profiler has no render to profile, the error overlay gives you a line but not the shape of the code around it. They all need the app to run first. It didn't.

The one moment you most need to understand your code’s structure is the one moment your code refuses to produce it.

Zero build, instant display

The Dependency Graph skips the build step entirely. Open a file and it parses the import tree to an AST and draws it in real time - no bundling, no dev-server round-trip, no waiting. That alone is the everyday time-saver: you see the whole reachable module graph the instant you ask for it, not after a rebuild.

And because the map is parsed from source rather than observed from a run, it has a property no runtime tool can match: it still renders when the canvas has completely crashed. A broken page isn’t a dead end — it’s the graph’s primary entry point.

Two ways to know a codebase

There are only two ways for a tool to learn the shape of your project, and the difference decides everything about when the tool is available to you.

Runtime introspection — needs a successful render. React DevTools, profilers, error overlays. Reads the live fiber tree after mount, knows real prop values — but only if they exist. Goes completely dark when the render throws, and can’t see a module that failed to initialize.

Static AST analysis — needs nothing to run. CrossUI’s Dependency Graph parses each file to an AST and reads its imports. Knows structure — every edge, before execution — indifferent to whether the app renders, and maps the broken module and its neighbors.

Runtime tools are richer when they work. But they are conditional on success. Static analysis is poorer in some ways — it can’t tell you what a variable held at 2:04pm — but it is unconditional. It works on a codebase that has never once rendered cleanly. That property is the whole point.

Reading the graph

Set any file as the ENTRY and the panel scans outward from it. Every node is tagged so you can place it at a glance: Entry, 📄 Local, 🌐 Ext, and ⟳ Cyclic.

Hover or select a node and animated lines trace its relationships in two directions — and the direction is the whole point:

  • Solid lines — downstream. The modules this file imports.

  • Dashed lines — upstream. The parents that import this file.

The search box highlights matching nodes live; the failing module blinks so your eye lands on it without hunting.

Click any node and the inspector slides out — your troubleshooting console for that file. It shows the resolved path, the raw source string and its resolvedSource, and, when parsing hit trouble, the exact error in red with line and column:

Inspector · TreeSitter.js 📄 Local
specifiers { parserInstance }
source ./TreeSitter.js
resolvedSource Engine/src/TreeSitter.js
error Parser engine initialization aborted · Line 32, Column 12
interceptor [ intercept source... ]
override [ override resolved path... ]

Navigation note: mouse-wheel pans vertically, Shift+wheel pans horizontally, Ctrl+wheel zooms on the cursor - so a huge graph stays fast to move through.

Cycles, in red and one click away

The engine detects circular references and draws the offending edge in red. A Show cycles filter in the bottom-left isolates just the nodes caught in a loop. Inside the inspector, a node in a cycle shows the full closed-loop chain — and hovering the chain highlights every related node on the canvas, while clicking a name in the chain selects and focuses that file. Circular imports are the quiet cause of a whole genre of bugs — a module that’s undefined at first access, lazy chunks that won't split, "works on the second hot-reload" gremlins. The graph just shows you the loop instead of making you find it by accident.

The part that’s actually new: bypass the broken module

Finding the blinking node is good. But the Dependency Graph goes one step further than any read-only map: it lets you swap the broken dependency for a mock and bring the render back — without editing the file and without a rebuild. Two injection levers, both in the inspector:

  • Interceptor — match on the import ... from 'yyy' declaration. The source string shown on the node is exactly the word you intercept.

  • Override — match on the final physical path the system resolved to. The resolvedSource on the node is exactly the path you override.

Configure either, then click the highlighted Apply Dependency Injection button in the top toolbar. Studio injects your rule into the current rendering context and attempts to restore the visualization — the deep, crashing module is now standing in for itself with something that renders. And the rules auto-persist, keyed by the entry file: reopen the same page tomorrow and your injection is already in place.

A read-only graph tells you where it broke. This one lets you route around the break and keep working — the file untouched, the page alive.

The render-error resolution flow

  1. Locate the blinking node. Open the graph from the error overlay’s Show Dependency Graph link. The module that crashed the render is already blinking.

  2. Investigate the root cause. Follow the line/column on the node, double-click to open that exact file, and find the offending code.

  3. Isolate with injection. If a deep dependency is the cause, set an interceptor (on source) or override (on resolvedSource) in the inspector to swap it for a mock.

  4. Apply to restore the render. Click Apply Dependency Injection. The rule is injected into the rendering context and the page attempts to come back — no rebuild.

  5. Persistence handles the rest. Your rules are saved locally against this entry file and restored automatically next time. Re-enter nothing.

Dependency graph — CrossUI Studio

Why this is the same thesis as everything else

If you read our first post, this will feel familiar. The reason a CrossUI visual edit produces a one-line git diff is the same reason the Dependency Graph survives a crash and can inject around it: we treat your parsed source as the source of truth, and every surface is just a view over that AST.

The canvas is a view over the AST. The prop inspector is a view over the AST. The Dependency Graph is a view over the AST across files — and injection is a controlled rewrite of how one edge resolves. None of it depends on your app successfully running, because none of it is built on the runtime. That’s a constraint we chose on purpose — it’s harder to build, and it’s exactly what keeps the tooling alive in the moments runtime tools abandon you.

What it can’t do — honestly

Static analysis buys unconditional availability by giving up runtime knowledge. The boundaries are real and we’d rather name them:

  • Fully dynamic imports are unresolvable. A dynamic import('./pages/' + name) with a runtime-computed path can't be resolved statically - we mark the edge rather than guess wrong.

  • It shows structure, not values. The graph tells you AIPanel reaches TreeSitter; it can't tell you what the parser returned this particular render. Injection mocks the dependency; it doesn't replay the runtime state.

  • Exotic resolution needs config. Non-standard bundler aliases or monorepo path magic resolve best when we can read your tsconfig / jsconfig. Without it, some edges fall back to raw specifiers.

None of those change the core promise: the graph is there when you’re stuck, and it can route you around the break — because it never needed your code to run.

Open Studio — crash a deep dependency, watch the graph point at it, then inject a mock and click Apply.

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.

Try it free → studio.crossui.com


Originally published at https://blog.crossui.com on June 5, 2026.

3 Comments

  1. 1

    What stood out to me is that a lot of the post depends on a very specific boundary between the problems AI solves well and the problems it doesn't.

    I'd be curious whether users naturally arrive with that same boundary in mind, or whether that's something they're discovering through use.

    1. 1

      Good question, honestly the more interesting one.

      from what i see, almost nobody comes in already knowing that boundary. they find it by frustration. they use AI for everything, works great for a while, then they ask for a tiny spacing change and get back a 200 line rewrite — and that is the moment it clicks for them.

      so i cant really teach the line up front, people dont believe it until they feel it. the tool just meets them at that point, after they already hit the wall themselves. and the line keeps moving as models get better anyway, so everyone kind of re-discover it for their own workflow.

      1. 1

        That makes sense.

        The part I'd be curious about is whether the frustration itself is the thing users are seeking help with, or whether it's simply the moment they become aware of a problem that already existed.

        Those can end up creating very different expectations for the product.

June 22, 2026 Your AI writes the logic. Studio tunes the UI. They don't compete.

I was building a dashboard. The card layout was almost right — just needed a bit more breathing room, a tighter border radius on the inner rows, and the label font to feel slightly lighter. Three small things.

I typed the description into Cursor. It came back with 180 lines rewritten. Every sx prop touched, four components restructured, a new wrapper div appeared. The layout looked the same. The diff was enormous.

I hit Ctrl+Z and went back to staring at the code.

That was the moment I stopped thinking of AI as a universal tool and started thinking of it as a specific tool — an exceptionally good one, for specific things.

What AI is actually good at

AI editors are brilliant at the parts of React development that are fundamentally about thinking: data flow, state architecture, API wiring, conditional rendering logic, form validation, accessibility semantics, refactoring towards patterns. These are tasks where the right answer involves reasoning across the whole codebase, holding context, understanding intent.

When I ask an AI to “add optimistic update to this mutation handler” or “extract this form logic into a custom hook” — I get back something genuinely useful, usually on the first try.

What AI is genuinely bad at

UI polish is a different category. Not because AI can’t read CSS — it clearly can. But because getting a layout exactly right is an iterative, visual, trial-and-error process. You need to see the result immediately. You need to tweak a value, check it, tweak again.

The AI-for-UI workflow is fundamentally broken:

  1. Describe what you want in words (already lossy)

  2. Wait for the model to generate a response

  3. Review 200 lines of diff

  4. Wait for the build

  5. See the result — probably not right

  6. Repeat, burning tokens each round

The bigger the model’s context about your component, the more it rewrites. It doesn’t know how to make a surgical one-property change, because its job is to understand and regenerate, not to minimally diff.

AI is expensive at precision. It’s cheap at breadth. UI tuning needs precision. Logic needs breadth. Match the tool to the task.

Where each tool belongs

AI Editor (Cursor / Claude / Copilot) — Component architecture and refactoring — State management and data flow — API integration and async logic — Accessibility and semantics — Test generation — Large-scale rewrites and migrations

CrossUI Studio — Spacing, sizing, layout tweaks — MUI sx and responsive breakpoints - Color, typography, visual hierarchy - Deep-layer UI drill-down and isolation - Rapid iterative visual feedback - Surgical one-property diffs

Notice there’s no overlap. That’s not accidental — these tools solve genuinely different problems. The goal was never to have one tool do everything. It was to cover the full surface of React development without gaps.

The pairing in practice

Since we shipped Local Folder access in v0.9.6, the workflow is concrete: point both your AI editor and Studio at the same folder on your machine. They work on the same files simultaneously. One writes logic, the other tunes UI. The diff in Git is always clean because every change is surgical.

Step 1 — AI builds the component Cursor or Claude writes the data-fetching hook, the conditional render, the event handlers. Full context, full reasoning, exactly what it’s good at.

Step 2 — Studio opens the same folder No upload, no sync, no token. Studio reads the files directly off disk — the canvas shows your component live.

Step 3 — You tune visually Adjust spacing, colors, and layout directly on the canvas. The code updates in real-time. One property changed = one property in the diff.

Step 4 — PR lands clean Logic changes from AI. UI changes from Studio. Both are minimal, reviewable, and traceable. No 200-line rewrites for a border-radius tweak.

What this means for “AI replacing developers”

There’s a lot of anxiety right now about AI eating programming jobs. Some of it is warranted — the volume of boilerplate code a developer needs to write by hand has dropped dramatically, and it’s not coming back.

But the anxiety assumes AI is a single homogeneous capability that scales linearly. It isn’t. There are things AI does cheaply and well (reasoning, pattern-matching, generation at scale) and things it does poorly or expensively (precise iterative feedback loops, visual judgment, knowing when to stop).

The developer who understands this boundary — and builds a workflow around it — is faster than the developer who either ignores AI entirely or tries to use it for everything. The job doesn’t disappear. It shifts: less time writing boilerplate, more time designing systems and judging quality.

Studio is a bet on that shift. Not a replacement for AI. Not a rejection of it. A tool for the part of your work that AI handles badly — so you can let AI fully own the part it handles well.

If you’ve been using an AI editor and hitting the same wall on UI iteration — try the pairing. Open Studio, point it at your project folder, and see if the division of labor clicks for you.

No account needed — Guest mode opens a workspace instantly. Or connect a local folder and work on your real files.

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.

Try it free → studio.crossui.com


Originally published at https://blog.crossui.com on June 19, 2026.

4 Comments

  1. 1

    This is one of the more serious attempts at solving the “visual editor vs real codebase” split, especially the AST-level sync angle. Most tools underestimate how much engineers care about diff purity and not just visual correctness.

    The real question is not whether the canvas works, it is whether teams trust a second editor touching production code paths without slowly eroding codebase ownership discipline. That adoption friction is usually stronger than the technical win.

    1. 1

      This is exactly the right question and honestly the part i think about most.

      you put it better than i do — teams don't care if the canvas works, they care if a second editor erodes their ownership. we built the whole product around this, we call it "developer sovereignty" on the site, and we put it in writing: no proprietary runtime, no closed-source deps, no platform-locked logic injected into your files. stop using Studio tomorrow and your project is still a standard React project, zero migration.

      the trust comes from the diff, not the canvas. a padding change is one line in the diff, same as if the dev typed it, so the source never stops being the source of truth and code review works exactly like before. the moment diffs get messy the whole thing collapses, you're 100% right.

      really appreciate you engaging at this level, this is the kind of feedback that shapes the product 🙏

      1. 1

        Developer sovereignty is a strong framing, and the no-lock-in promise plus one-line diffs go a long way toward solving the trust problem on paper. The part I'd still want proof of as an engineering lead is more social than technical would my team actually believe that every canvas edit stays one line, or would they assume drift over time and start reviewing Studio-touched files more carefully than AI-touched ones?

        That kind of trust usually isn't won by the feature it's won by track record teams seeing months of clean diffs in the wild before they stop double-checking. Are you seeing that pattern yet with early users who've stuck around?

        1. 1

          That's a fair observation. Trust is usually earned over time, not through product claims. My view is that teams shouldn't trust Studio because we say "one-line diffs." They should trust it because every change remains visible in Git, code review, and their existing workflow. The promise is not "don't check our work," but rather "you can check our work easily."

          One thing that helps is that Studio can work directly with local files. The actual source files remain on disk and can be opened, edited, compared, committed, or reverted using whatever tools the team already uses. In that sense, Studio isn't asking teams to trust a black box. The source of truth remains the code itself.

          We're starting to see that pattern with some long-term users, although we're still early. Interestingly, the feedback isn't that people stop reviewing Studio-generated changes. It's that after enough clean diffs, they stop treating them differently from hand-edited code.

About

AI writes great logic but rewrites 200 lines for a tiny UI tweak. CrossUI Studio is the visual half: tune UI on a canvas, get a clean one-line diff in your real code. No export, no lock-in.