DEV Community

Cover image for Building an Accessible Architecture Diagram Viewer with Archify, React, and Tailwind CSS
Chloe Chen
Chloe Chen

Posted on

Building an Accessible Architecture Diagram Viewer with Archify, React, and Tailwind CSS

Building an Accessible Architecture Diagram Viewer with Archify, React, and Tailwind CSS

At 2:47 AM, a screen reader user reported they couldn't close your architecture diagram modal—focus was trapped behind the overlay, and the only way out was refreshing the page. Worse, your performance monitoring showed 340ms of layout thrashing every time someone opened a large diagram. These aren't edge cases. They're production failures that stem from treating a diagram renderer as a simple <div> insert when it's actually a complete document with motion, export controls, and its own interaction model.

I evaluated tt-a1i/archify as an external integrator to solve one specific problem: displaying a motion-enabled, self-contained architecture diagram inside a React application without breaking screen reader navigation or forcing repeated layout recalculation. Archify's useful boundary is its generated HTML artifact. The diagram owns rendering, animation, and export. The host application owns routing, permissions, loading state, and the accessible shell around the artifact. Keeping those responsibilities separate prevents a diagram renderer from becoming a second application framework inside your product.

The production failure mode

A common implementation animates diagram container width and height from JavaScript. Every frame, the browser recalculates layout, repaints the viewport, and composites layers. On diagrams with 40+ nodes, this creates visible jank—especially when users resize the preview or change zoom levels.

The second failure is silent but more destructive: a hand-rolled modal often leaves focus behind the overlay. A screen reader user continues navigating the page while the diagram appears open. Pressing Escape may close the visual layer without returning focus to the trigger. The user is now lost in the DOM with no audible indication of where they are.

The following component uses Radix Dialog for focus trapping, Escape handling, focus restoration, and ARIA semantics. Archify's generated HTML is passed as srcDoc, appropriate when the artifact comes from a trusted build step. The iframe receives a meaningful accessible name and a restrictive sandbox. If the diagram needs scripts, allow-scripts is sufficient—avoid adding allow-same-origin unless the artifact genuinely requires it.

import * as Dialog from "@radix-ui/react-dialog";
import { X } from "lucide-react";

type ArchitecturePreviewProps = {
  title: string;
  description?: string;
  html: string;
};

export function ArchitecturePreview({
  title,
  description = "Interactive architecture diagram preview",
  html,
}: ArchitecturePreviewProps) {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        <button
          type="button"
          className="rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white
                     shadow-sm transition hover:bg-slate-700 focus:outline-none
                     focus-visible:ring-2 focus-visible:ring-blue-500
                     focus-visible:ring-offset-2"
        >
          Open diagram
        </button>
      </Dialog.Trigger>

      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 z-40 bg-slate-950/70 data-[state=open]:animate-fade-in" />

        <Dialog.Content
          aria-describedby="architecture-preview-description"
          className="fixed inset-4 z-50 flex flex-col overflow-hidden rounded-xl
                     border border-slate-200 bg-white shadow-2xl outline-none
                     sm:inset-8 lg:inset-12"
        >
          <div className="flex items-start justify-between gap-4 border-b border-slate-200 p-4">
            <div>
              <Dialog.Title className="text-base font-semibold text-slate-950">
                {title}
              </Dialog.Title>
              <Dialog.Description
                id="architecture-preview-description"
                className="mt-1 text-sm text-slate-600"
              >
                {description}
              </Dialog.Description>
            </div>

            <Dialog.Close asChild>
              <button
                type="button"
                aria-label="Close diagram preview"
                className="rounded-md p-2 text-slate-500 hover:bg-slate-100
                           hover:text-slate-900 focus:outline-none
                           focus-visible:ring-2 focus-visible:ring-blue-500"
              >
                <X aria-hidden="true" size={18} />
              </button>
            </Dialog.Close>
          </div>

          <div className="diagram-frame min-h-0 flex-1 bg-slate-50 p-3">
            <iframe
              title={`${title} architecture diagram`}
              srcDoc={html}
              sandbox="allow-scripts"
              loading="lazy"
              className="h-full w-full rounded-lg border border-slate-200 bg-white"
            />
          </div>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}
Enter fullscreen mode Exit fullscreen mode

The critical detail isn't the modal styling—it's the ownership model. Radix manages interaction state and focus. The iframe isolates Archify's document and prevents diagram styles from leaking into the host application. Tailwind handles the stable shell without requiring JavaScript measurements.

Configuration as an explicit build boundary

Generate the diagram before deployment or in a controlled server-side job, then supply it to the React component as an artifact. A small configuration file makes this boundary reproducible:

# archify.config.yaml
input: ./architecture/system.md
output: ./public/diagrams/system.html
theme: slate
motion:
  enabled: true
  durationMs: 420
  prefersReducedMotion: true
export:
  formats:
    - html
    - svg
  width: 1920
  height: 1080
security:
  sandbox: allow-scripts
Enter fullscreen mode Exit fullscreen mode

The exact command depends on the repository's CLI surface, but the architectural intent remains stable: source content transforms into a self-contained HTML deliverable, and the React application consumes that deliverable rather than reconstructing the diagram at runtime.

CSS micro-interaction without layout thrashing

Use transforms and opacity for interaction feedback. Reserve container queries for responsive behavior inside the component rather than reading element dimensions in JavaScript.

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(0.35rem) scale(0.99);
  }

  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes diagram-pulse {
  0%,
  100% {
    box-shadow: 0 0 0 0 rgb(59 130 246 / 0);
  }

  50% {
    box-shadow: 0 0 0 0.35rem rgb(59 130 246 / 0.12);
  }
}

@container (min-width: 48rem) {
  .diagram-frame {
    padding: 1.25rem;
  }

  .diagram-frame iframe {
    animation: diagram-pulse 2.8s ease-in-out 1;
  }
}

@media (prefers-reduced-motion: reduce) {
  .diagram-frame iframe,
  [data-state="open"].data-\[state\=open\]\:animate-fade-in {
    animation: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

In a Tailwind v4 project, register the animation utility in the application stylesheet if it isn't already available:

@theme {
  --animate-fade-in: fade-in 180ms ease-out both;
}
Enter fullscreen mode Exit fullscreen mode

This avoids the tempting but expensive pattern of updating inline width and height on every pointer event. The browser can usually composite transforms without recalculating the entire document layout.

Practical trade-offs

An iframe adds a document boundary and makes cross-document communication less convenient. That's a worthwhile cost when Archify exports a complete HTML experience with its own motion and controls. A direct React renderer would provide tighter state integration, but it would also couple the application to diagram internals and increase the chance of CSS, focus, and lifecycle conflicts.

A trusted srcDoc artifact is also a security boundary only when its generation pipeline is controlled. Don't interpolate user-authored HTML without sanitization. If diagrams are user-generated, sanitize before storage and consider serving them from a separate origin with a restrictive Content Security Policy.

The resulting architecture is deliberately modest: Archify produces the visual artifact, the iframe isolates it, Radix owns modal accessibility, and Tailwind styles the host shell. That separation addresses the real failure modes—layout thrashing and broken focus navigation—without turning a diagram preview into an unmaintainable custom UI subsystem.

The hardest operational dilemma here is deciding when the iframe boundary becomes more costly than direct integration. If your diagrams need bidirectional state sync with the host application—live editing, real-time collaboration, or deep keyboard navigation across diagram nodes—the document boundary starts working against you. At that point, you're building a tightly coupled diagram editor, not a viewer, and the architecture needs to change accordingly.

How are you handling architecture diagram rendering in production applications? Have you hit the layout thrashing ceiling, or are you dealing with something worse—focus traps, memory leaks from unmounted SVG, or accessibility complaints you can't reproduce? I'd like to hear what broke and how you fixed it.

Top comments (0)