How Velo™ Works: Dynamic Rendering Strategies
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:

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
- Create a new React component
- Register it in the component registry
- 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.