How Velo™ Works: Dynamic Rendering Strategies

How Velo™ Works: Dynamic Rendering Strategies
ZenBusiness VELO™

Building a Server-Driven UI Without Losing Frontend Control

Imagine you're building a chat-based assistant UI. One that has to juggle simple messages, interactive carousels, buttons, forms, and the occasional error state. And it needs to do all of that gracefully, even in long-running conversations, without turning into a tangle of conditionals and custom components.

Now add this constraint: backend and AI teams need to control what gets shown and when, without triggering a frontend deploy every time a new component or flow is added. That was our challenge.

We knew this UI needed to be:

  • Flexible: Should support dozens of rich, interactive component types.
  • Performant: Must stay smooth as conversations grow longer.
  • Developer-Friendly: Backend teams should ship UI changes independently.

Our answer was to treat the UI as data. We built a system where the backend sends structured UI instructions, and the frontend renders them using a registry-based architecture. It's dynamic and declarative, but still gives the frontend full control over design and UX polish.

The Core Idea: UI as Data, Not Logic

At the heart of our approach is this principle:

The backend decides what to show. The frontend decides how to show it.

That means the backend and AI systems send JSON instructions that describe UI components to render, like:

{
  "componentType": "Message",
  "props": {
    "text": "Welcome to your dashboard!"
  }
}

The frontend renders using a registry-based architecture. That includes layout, styling, theming, animation.

This gives us the best of both worlds:

  • The UI remains consistent and tightly coupled to the design system.
  • Backend and AI teams can drive dynamic conversations without frontend coordination.

Key Building Blocks

Here’s the vocabulary we use in our architecture:

  • RenderInstruction: A JSON object describing a single UI component to render.
  • Component Registry: A map of component types to React components.
  • Instruction Group: A logical bundle of instructions rendered together (e.g., Message + Carousel + Buttons).
  • Render Strategy: Controls how instructions are inserted or updated (e.g., append_to_chat, replace_component).

Our 4-Layer UI Architecture for Dynamic UIs

To make this work at scale, we split the frontend into four clean layers:

4-Layer UI Architecture

1. Data Flow Layer

Manages communication, state, and domain logic:

  • API calls for data fetching and mutations
  • Session management (starting, continuing, tracking state)
  • Context updates for user sessions
  • Optimistic updates and retries

2. Instruction Processing Layer

Prepares render instructions:

  • Validates input payloads
  • Coordinates UI state (e.g., hiding buttons after click)
  • Manages animation sequencing and lifecycle

3. Component Registry Layer

The lookup table that powers dynamic rendering:

export const componentRegistry = {
  MESSAGE: MessageComponent,
  CAROUSEL: CarouselComponent,
  BUTTON_PROMPT: ButtonPrompt,
  // ...19+ components
};
  • Ensures type safety
  • Provides helpful dev-time warnings
  • Keeps components reusable and decoupled

4. Rendering Engine

Responsible for visuals and user interaction:

  • Animates with Framer Motion
  • Memoizes components for performance
  • Handles viewport and scroll behavior

How We Group and Animate Dynamic UI Components

Instructions are bundled by the backend into instruction groups.
Server determines logical groupings based on conversation flow and sends them as instruction groups

  • Are animated together as a block with sequential components animation
  • Use render strategies to define behavior

A sample instruction group:

{
  "componentType": "Message",
  "props": {
    "variant": "card",
    "message": "Based on your business type..."
  }
},
{
  "componentType": "Carousel",
  "props": {
    "articles": [ ... ]
  }
},
{
  "componentType": "ButtonPrompt",
  "props": {
    "message": "No Questions",
    "appearance": "primary"
  }
},
{
  "componentType": "ButtonPrompt",
  "props": {
    "message": "How will you notify me?",
    "appearance": "secondary"
  }
}

Render Strategies

  • Append to Chat: Default – new groups appear at conversation bottom
  • Replace Component: Updates specific components
  • Replace Page: Full takeovers for workflows or errors

Smart component coordination

  • Sequential Animation: Within each group, components animate in sequence, not simultaneously
  • Animation Callbacks: Allows components like CheckIn to trigger the next sequence via onAnimationComplete
  • Button State Management: Hides old buttons when users interact, preventing conflicting actions.
  • Render Keys: Dynamic keys ensure React properly re-renders components even when content is similar

This gives server control over conversation flow while maintaining smooth client-side interactions and predictable animation sequences.

Performance Techniques for Long Chat UIs

Performance isn’t an afterthought here. We had to build for:

  • Long-running sessions
  • Rapid updates
  • High interaction density

Here’s what helped:

  • Memoization + stable keys
  • Avoiding re-animation with ref tracking
  • IntersectionObserver to trigger scroll-based animations
  • Centralized animation logic in the rendering engine

Why the Registry Pattern Wins

This was one of our best architectural decisions:

  • Zero-Touch Scaling: Backend and AI systems can use new components instantly
  • Type-Safe: Errors surface early in dev
  • Fail-Safe: Unknown components are ignored (not crashed)
  • Reusability: Component logic is modular and testable

Developer Experience: Benefits for Every Role

Role

Primary Benefit

Secondary Benefit

Backend Engineers

Can shape UI without needing deploy access

Focus on domain logic

AI Engineers

Can experiment with conversation flows and components directly

Build, test, and iterate on personalized UX strategies

Frontend Devs

Clear contracts and shared logic

Improved performance and reuse

Product & Design

Rapid prototyping, consistent experience

Server-driven experimentation

Making It Robust: Error Handling

Even flexible systems need safety nets:

  • Unknown Components: Ignored in prod, warned in dev
  • Invalid Instructions: Filtered before render
  • Error Recovery: Users can retry conversations with one tap or start fresh.

The goal: never block the user with a rendering bug.

Adding a New Component Is Stupid Simple

  1. Create a new React component
  2. Register it in the component registry
  3. Add its type constant

componentRegistry.POLL = PollComponent;

The backend can start sending it immediately.

What This Architecture Enabled

  • 19+ components dynamically driven by backend and AI logic
  • Zero frontend deploys for most UI updates
  • A/B testing and personalization via server control

Lessons & Future Opportunities

What Worked

  • Registry pattern gave us scalability and type safety
  • Treating UI as data let us iterate without blocking frontend teams
  • Group-aware animation made chat feel smooth and alive

What’s Next

  • Caching for faster state restoration
  • Advanced animation presets
  • Progressive loading of long histories
  • Smarter error recovery

Final Thoughts

If you're building a system where the UI needs to adapt to backend logic, whether that's chatbots, onboarding flows, or dynamic dashboards, this pattern is worth exploring. By separating rendering logic from orchestration and creating clean contracts between backend, AI, and frontend teams, we shipped faster, built more reliably, and kept the UI polished.

Have you tried building server-driven UIs? We'd love to hear how you approached dynamic rendering and what worked (or didn’t) for you.

We’re building ZenBusiness Velo in the open. We hope you’ll join us.

Read more