DEV Community

Cover image for Container Queries and Focus Management: Building Resilient Components with TailwindCSS v4
Chloe Chen
Chloe Chen

Posted on

Container Queries and Focus Management: Building Resilient Components with TailwindCSS v4

Container Queries and Focus Management: Building Resilient Components with TailwindCSS v4

When integrating meta/astryx into a production TypeScript stack last quarter, we hit a painful wall: our card components looked pristine in desktop viewports but collapsed into unusable layouts when nested inside sidebars or widget containers. The root cause wasn't the design system itself—it was our reliance on viewport-based media queries instead of container queries. Combined with improperly managed focus trapping in modal overlays, our accessibility score tanked.

Let's walk through the hard-won fixes.

The Container Query Problem

Viewport-based breakpoints assume a global coordinate system. When meta/astryx components lived inside a max-w-xs sidebar, our md: and lg: classes fired anyway, rendering a responsive grid that had no space to breathe.

TailwindCSS v4 gives us container queries out of the box:

// Mark a container context
<div className="@container">
  {/* Children respond to container width, not viewport */}
  <div className="@sm:grid @sm:grid-cols-2 @lg:grid-cols-4">
    {/* Card components resize based on container, not screen size */}
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

In your Tailwind config, container queries are enabled by default in v4. The @sm, @md, @lg prefixes correspond to container widths (640px, 768px, 1024px).

Practical Component Example

Here's a reusable card component that adapts to its container context:

export function ResponsiveCard({
  title,
  children,
  variant = 'default'
}: {
  title: string
  children: React.ReactNode
  variant?: 'default' | 'elevated'
}) {
  return (
    <article
      className={`
        @container rounded-lg border border-slate-200
        ${variant === 'elevated' ? 'shadow-lg' : 'shadow-sm'}
        p-4 @sm:p-6 @lg:p-8
      `}
    >
      <h2 className="@sm:text-lg @lg:text-xl font-semibold text-slate-900">
        {title}
      </h2>
      <div className="mt-4 @sm:mt-6 @lg:grid @lg:grid-cols-2 @lg:gap-4">
        {children}
      </div>
    </article>
  )
}
Enter fullscreen mode Exit fullscreen mode

The beauty: drop this component anywhere—sidebar, modal, main content—and it responds intelligently to available space.

Focus Trapping and Modal Accessibility

Our second catastrophe: screen readers navigated straight through our modal overlay into the background DOM. Users with keyboard-only input couldn't escape modals without tabbing through 200 hidden elements.

Using Radix primitives (which meta/astryx often builds on), focus management becomes declarative:

import * as Dialog from '@radix-ui/react-dialog'

export function AccessibleModal({
  isOpen,
  onOpenChange,
  title,
  children
}: {
  isOpen: boolean
  onOpenChange: (open: boolean) => void
  title: string
  children: React.ReactNode
}) {
  return (
    <Dialog.Root open={isOpen} onOpenChange={onOpenChange}>
      <Dialog.Portal>
        {/* Backdrop with inert attribute prevents focus escape */}
        <Dialog.Overlay
          className="fixed inset-0 bg-black/50 data-[state=open]:animate-fadeIn"
          aria-hidden="true"
        />
        <Dialog.Content
          className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-96 max-w-screen rounded-lg bg-white p-6 shadow-xl focus:outline-none"
          role="dialog"
          aria-labelledby="modal-title"
          aria-modal="true"
        >
          <Dialog.Title id="modal-title" className="text-lg font-semibold">
            {title}
          </Dialog.Title>
          <div className="mt-4">{children}</div>
          <Dialog.Close
            className="absolute right-4 top-4 p-2 text-slate-500 hover:text-slate-900 focus:ring-2 focus:ring-blue-500"
            aria-label="Close dialog"
          >
            ✕
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  )
}
Enter fullscreen mode Exit fullscreen mode

Radix handles focus trap internally: Tab cycles only within the modal, Escape closes it, and screen readers announce role and title on entry.

Micro-Interaction: Smooth Dismissal

Combine container queries with a discrete fade animation:

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@supports (animation-timeline: view()) {
  .modal-enter {
    animation: fadeIn 200ms cubic-bezier(0.16, 1, 0.3, 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Tailwind's data-* selectors make this trivial:

className="data-[state=open]:animate-fadeIn"
Enter fullscreen mode Exit fullscreen mode

Integration with meta/astryx

When testing meta/astryx's component library, ensure your container context is set at the layout boundary—typically your main content wrapper. This prevents collision between the design system's internal breakpoints and your application's responsive strategy.

// pages/dashboard.tsx
export default function Dashboard() {
  return (
    <div className="@container flex gap-6">
      <aside className="w-64">
        {/* Sidebar + cards inside 256px container */}
      </aside>
      <main className="flex-1">
        {/* Main area responds to its own container width */}
      </main>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The Real Tension

Container queries solve isolation beautifully, but they carry a performance cost on older browsers and complex nesting scenarios. Radix's focus management is bulletproof for accessibility compliance, yet it adds another dependency to your bundle. The trade-off: pick your battles. For component libraries like meta/astryx that ship into varied contexts, container queries are non-negotiable. For highly controlled admin dashboards, viewport breakpoints might suffice.

The question isn't which is objectively better—it's whether your integration pattern demands resilience in unknown containers or predictable, controlled layouts.

Top comments (0)