design engineering rectangles

About the author & background

Hi, I'm Cole. At Phosphor, we're tackling extremely dense UI problems—the kind where every rectangle becomes a hydra.

I've gone from basic MVC to MVVM, then at my previous startup, we ventured into Rust. The borrow checker led us to entity-component-systems (ECS), a pattern from game development where entities are just IDs, components are pure data in sparse sets, and systems provide behavior.

What this yielded: files that needed complex logic could import just the components they cared about. One system handled raw structure to parsed form. Another took parsed form to errors. A third mapped errors to UI notifications. Each system was extremely scoped, only caring about the components between them.

I loved this pattern not just for Rust reasons, but for the properties it gave me for testing and reasoning. Early on at Phosphor, I suspected we could leverage this to improve our UI architecture.

The Rectangle Hydra Problem

Let me paint the picture of what we're experiencing.

Start with one input box: some hooks, state management, maybe persistence. Add validation that reports errors. Now you need system-wide undo/redo, which touches multiple files. Then collaborative editing where multiple people see each other's changes in real-time—this touches your sync layer, your frontend hooks, everything.

And these are just table stakes. Basic features for any authoring environment.

But things don't stop there. At Phosphor, we add comparisons (single value vs. version-controlled state), origin tracking (where every value came from), diff views, key bindings, selection state, copy/paste... the list continues.

Each feature touches the same rectangles. Without the right architecture, you're rewriting yesterday's code to add tomorrow's feature.

Modern UI isn't a form with buttons. It's a world of interacting concerns—presence, diffs, commands, AI, validation, keyboard, and more.

Here's the problem: how do you add these features without rewriting yesterday's code?

The Rectangle That Grew Up

Watch a simple rectangle evolve. This is a live WorldState demo—each scroll step toggles feature flags that plugins react to.

Base: A rectangle with a value.

Open devtools (Cmd/Ctrl + Click the rectangle) to see the entity with CValue component.

+ Validation: Add inline rules and formatting.

The ValidationPlugin activates, adding CValidationRules and CValidationState components.

+ Undo/Redo: Users expect time travel.

The UndoPlugin wraps value changes, adding CUndoStack component.

+ Diffs: Show what changed and why. "Changed from 42 by Alice, 2m ago"

The DiffPlugin adds CVersionComparison and CChangeOrigin components.

+ Presence: Two more cursors just entered the room.

The PresencePlugin adds CCollaborators component tracking cursor positions.

+ Selection & Focus: They're not the same thing.

Note: Selection (what's highlighted) ≠ Focus (where keyboard input goes). They can diverge, then sync.

The FocusPlugin adds CSelection and CFocus as separate components.

+ Commands: Actions follow context, not hard-coded switches.

The CommandsPlugin adds CActions component with context-aware command palette.

+ AI Suggestions: Assistive proposals without breaking your model.

The AIPlugin adds CSuggestions component without touching validation or undo logic.

+ Inline Viz: Tiny chart, big meaning.

The VisualizationPlugin adds CInlineChart component reading from CValue.

Reality check: If your app is a simple form—great. You don't need this.

But if your rectangles feel like living systems, keep reading.

Notice: All 9 plugins active. No coupling. Each feature is just a flag in CFeatureFlags.

Borrowed From Games

Game developers faced this exact problem a decade ago and found an elegant solution: entities, components, and systems. The pattern translates directly to UI engineering.

Entities are things in your UI world—a text input, a button, a data table. Not React components, but conceptual objects that can have multiple aspects.

Components are pure data attached to entities. A Position component stores coordinates. A Label component holds display text. A Selection component tracks what's highlighted. Crucially, components contain no behavior—they're just structured data.

Plugins provide behavior that reacts to component combinations. A ValidationPlugin watches entities with Value and ValidationRules components. A CursorPlugin manages entities that have both Position and Presence components. A FocusPlugin handles entities with Selection and Keyboard components.

The key insight: add features by adding plugins and components, not by editing old code. Your existing validation logic never changes when you add collaborative cursors. Your undo system remains untouched when you introduce AI suggestions.

Translating ECS to UI

Unlike Rust, we have reactive containers—signals, atoms—and we don't have frame-by-frame computation like games. Instead, we have components that contain atoms. When we create a plugin, it operates on the fact that if there's an entity with certain components, it can provide additional components. There's never a need to add or remove components dynamically. You simply have this plugin architecture.

At Phosphor, WorldState is backed by Jotai Atoms—reactive containers for values. We're considering adapting it to use Livestore as our reactivity graph next.

WorldState ≠ game engine. It's an ECS-inspired way to express UI concerns as composable data + behavior.

Two Proof Paths

1. Interactive WorldState Demo (see the devtools in action)

Try this:

  • Cmd/Ctrl + Click any entity card to inspect it in the devtools panel
  • Alt + Click for detailed component view
  • Watch how components (Position, Label) are isolated but composable
  • Nothing coupled—each feature is a plugin layer

What it proves: Entities with components. Dev panel inspection. Additive architecture in practice.

2. Keyboard/Focus (production pain, solved)

  • Context-sensitive shortcuts without coupling
  • Focus, selection, actions—cleanly composed
  • Grid navigation with live actions palette

What it proves: Selection ↔ Focus ↔ Actions interplay managed by components and parent-walking.

The Promise

You don't scale complex UI by burrowing deeper into components.

You scale it by spreading features horizontally:

  • Attach data
  • Register behavior
  • Let events find their handlers

WorldState helps you ship the "next layer" without touching yesterday's files.

Is This For You?

Great fit if building:

  • Collaborative editors
  • Dashboards with cross-panel effects
  • Complex modeling tools
  • Pro workflows with deep keyboarding

Probably overkill if:

  • A basic CRUD app meets your needs

Design engineers welcome: The more interaction layers, the more it shines.

The architecture pays for its complexity by preventing coupling debt. Early investment in entity-component structure pays dividends when you need to add the fifth, sixth, and seventh layers of interaction.


Your Rectangle Doesn't Have to Be a Hydra

We're collecting UI engineering war stories and solutions like this one. What's your rectangle horror story? What tools have you discovered that tame coupling nightmares in complex interfaces?

Share your dev tooling discoveries with us—we'll feature the best ones and build a knowledge base of battle-tested patterns for UI engineers.

The future of UI engineering is compositional. Let's build it together.

Check out our job listings if you're the kind of engineer who sees these patterns and wants to build tools that make them easier to implement.


Implementation Notes

Scrollytelling pattern
  • Sticky canvas on left; right-rail copy advances steps
  • Each step toggles a single "layer" (avoid visual clutter)
  • Layer chips bar at end: toggle all on/off for composition understanding
Motion language
  • Gentle, purposeful motion (250–400ms)
  • Single pulse/ripple motif for "cross-panel effects"
Accessibility
  • Each step reachable with ←/→ keys
  • ARIA live regions narrate visual changes
  • Respect prefers-reduced-motion
Performance
  • Lazy-load heavy assets
  • Canvas demos render only when in view
  • Avoid blurs/alpha stacking on large regions

Next Steps