cd /news/generative-ai/why-a2ui-s-json-driven-layout-schema… · home › topics › generative-ai › article
[ARTICLE · art-169338] src=dev.to ↗ pub= topic=generative-ai verified=true sentiment=↓ negative

Why a2ui's JSON-Driven Layout Schema Breaks Down in Production Modal Stacks

A developer evaluating the a2ui-project/a2ui generative UI framework found its JSON layout schema breaks down for nested modal stacks, as TailwindCSS utility classes fail to cascade into React portals and focus management collapses when modals are chained. The developer patched the React adapter to inject data-focus-scope attributes, but the fix requires maintaining a fork that must be manually reconciled with upstream changes between versions like v0.8.3 and v0.9.1.

by read3 min views3 publishedNov 14, 2026

When evaluating a2ui-project/a2ui for a conversational dashboard rebuild, the pitch was clean: backend agents stream JSON layout descriptors, and the React adapter hydrates them into live components. No manual createElement chains, no brittle template strings. The repository ships adapters for React, Angular, Lit, Flutter, SwiftUI, and Jetpack Compose—universal generative UI.

The reality hit during our first modal implementation. a2ui's layout schema handles flat component trees elegantly, but once you introduce nested modals with focus trapping and portal rendering, the abstraction starts bleeding through the seams.

Our agent streams this layout descriptor:

{
  "type": "modal",
  "id": "confirm-delete",
  "backdrop": true,
  "children": [
    {
      "type": "box",
      "className": "bg-white rounded-lg shadow-xl p-6 max-w-md",
      "children": [
        { "type": "text", "content": "Delete 47 items?" },
        {
          "type": "button",
          "variant": "danger",
          "label": "Confirm"
        }
      ]
    }
  ]
}

The React adapter wraps this in a Radix Dialog.Root. Problem: TailwindCSS utility classes don't cascade into React portals unless you explicitly configure tailwind.config.js to scan portal roots. The modal renders, but bg-white and shadow-xl vanish—the dialog appears as unstyled gray blocks.

Force Tailwind to scan portal containers:

// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
    './node_modules/@a2ui/react-adapter/**/*.js',
    // Critical: scan React portal mount points
    './public/index.html'
  ],
  theme: {
    extend: {}
  }
};

But now you've coupled your build pipeline to a2ui's internal DOM structure. When the adapter changes its portal strategy (observed between v0.8.3 and v0.9.1), styles break silently in production.

Agent streams a second modal ("Are you sure?") while the first is still open. a2ui's React adapter chains Dialog.Root components, but focus management collapses:

Tab inside the second modal.Escape key dismisses both modals simultaneously. Root cause: a2ui's layout schema has no explicit focus scope boundaries. The JSON descriptor treats modals as generic containers, delegating focus control entirely to the component library (Radix, in this case). When you nest portals, Radix's FocusScope gets confused by React's reconciliation order.

We patched the adapter to inject data-focus-scope attributes:

// Custom React adapter override
import { Dialog } from '@radix-ui/react-dialog';
import { useEffect, useRef } from 'react';

function A2UIModal({ descriptor, children }) {
  const scopeRef = useRef(null);

  useEffect(() => {
    if (scopeRef.current) {
      scopeRef.current.focus();
    }
  }, []);

  return (
    <Dialog.Root open={true}>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-black/50" />
        <Dialog.Content
          ref={scopeRef}
          data-focus-scope={descriptor.id}
          className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
        >
          {children}
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

This works, but now we're maintaining a fork of the adapter. Every upstream change requires manual merge conflict resolution.

The core tension: a2ui's JSON schema optimizes for agent-side simplicity (LLMs generate clean, validated layouts), but sacrifices client-side flexibility. When you hit edge cases—stacked z-index conflicts, viewport-aware positioning, touch gesture cancellation—you're forced to either:

Example: We tried adding a trapFocus: "strict" flag to the modal descriptor. The React adapter honored it, but the Flutter adapter ignored it (no equivalent in Flutter's showDialog). Cross-platform consistency collapsed.

If your generative UI flows are linear and non-overlapping (form wizards, chatbot threads), a2ui's abstraction holds. But production apps demand:

These patterns expose a2ui's unresolved question: should the layout schema stay minimal (forcing platform-specific hacks), or should it expand into a full declarative accessibility DSL (risking bloat and divergence from native primitives)?

No clean answer yet. The repository's issue tracker shows 14 open threads on focus management alone.

── more in #generative-ai 4 stories · sorted by recency
── more on @a2ui-project/a2ui 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/why-a2ui-s-json-driv…] indexed:0 read:3min 2026-11-14 · —