Beyond Media Queries

Mikey NicholsMikey Nichols
7 min read

In the early days of responsive web design, media queries revolutionized how we built websites. They gave us the power to adapt layouts based on viewport dimensions, allowing a single codebase to work across devices. But as web applications grow increasingly complex, the limitations of this approach have become evident.

Today, we'll explore how native browser APIs offer more sophisticated ways to create responsive experiences—ones that respond not just to screen sizes, but to individual element contexts, user preferences, and interaction patterns.

The Limitations of Viewport-Based Responsiveness

Traditional responsive design relies heavily on viewport dimensions to trigger layout changes. While effective for simple sites, this approach falls short in complex interfaces for several reasons:

  1. Components lack context awareness - A sidebar component doesn't know if it's in a narrow dashboard or a wide homepage

  2. Nested components can't respond independently - Elements within containers can't easily adapt to their immediate parent's dimensions

  3. User preferences are ignored - Device capabilities and user preferences often remain unaddressed

Element-Centric Responsiveness: A New Paradigm

Modern interfaces demand components that understand their own context. Instead of responding to the entire viewport, truly responsive components adapt to:

  • Their own dimensions and available space

  • Parent container constraints

  • User interaction patterns and preferences

  • Device capabilities beyond just screen size

Let's explore the native browser APIs that make this possible.

ResizeObserver: The Foundation of Element-Based Responsiveness

ResizeObserver provides the cornerstone for element-centric responsive design. Unlike window resize events, ResizeObserver lets you monitor specific elements and react when their dimensions change—regardless of what caused the change.

Here's how to implement a basic ResizeObserver:

// Create the observer
const resizeObserver = new ResizeObserver(entries => {
  for (const entry of entries) {
    // Get the element that was resized
    const element = entry.target;

    // Access its new dimensions
    const width = entry.contentRect.width;

    // Apply responsive behavior based on element size
    if (width < 400) {
      element.classList.add('compact');
    } else {
      element.classList.remove('compact');
    }
  }
});

// Start observing an element
const myComponent = document.querySelector('.responsive-component');
resizeObserver.observe(myComponent);

This simple pattern creates self-contained responsive components that adapt to their own dimensions rather than viewport breakpoints.

Creating Container Queries with ResizeObserver

While CSS container queries are gaining support, ResizeObserver offers a robust JavaScript alternative that works across browsers:

function createContainerQuery(element, breakpoints, callback) {
  const observer = new ResizeObserver(entries => {
    for (const entry of entries) {
      const width = entry.contentRect.width;

      // Determine which breakpoint applies
      let activeBreakpoint = null;
      for (const [name, size] of Object.entries(breakpoints)) {
        if (width <= size) {
          activeBreakpoint = name;
          break;
        }
      }

      // Call the callback with the active breakpoint
      callback(activeBreakpoint, width);
    }
  });

  observer.observe(element);
  return observer; // Return for cleanup later
}

// Usage example
const container = document.querySelector('.card-container');
createContainerQuery(
  container,
  { small: 300, medium: 600, large: 900 },
  (breakpoint, width) => {
    container.dataset.size = breakpoint || 'xlarge';
    // Additional responsive behavior...
  }
);

This approach enables true component-level responsiveness without relying on global viewport conditions.

Adapting to User Preferences with MediaQueryList API

Beyond element dimensions, truly responsive interfaces adapt to user preferences. The MediaQueryList API brings the power of media queries to JavaScript:

// Check for dark mode preference
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');

function applyTheme(isDarkMode) {
  document.documentElement.classList.toggle('dark-theme', isDarkMode);
}

// Apply immediately
applyTheme(darkModeQuery.matches);

// Listen for changes
darkModeQuery.addEventListener('change', event => {
  applyTheme(event.matches);
});

This API allows you to respond to various user preferences:

  • prefers-color-scheme for light/dark mode

  • prefers-reduced-motion for animation sensitivity

  • prefers-contrast for accessibility needs

  • prefers-reduced-data for connection limitations

Building Touch-Friendly Interfaces with Pointer Events

Modern interfaces must work seamlessly across interaction methods. The Pointer Events API unifies mouse, touch, and pen inputs:

const element = document.querySelector('.interactive-element');

// Single unified event handler for all pointer types
element.addEventListener('pointerdown', event => {
  // Access information about the input device
  const isTouch = event.pointerType === 'touch';
  const isPen = event.pointerType === 'pen';
  const isMouse = event.pointerType === 'mouse';

  // Adapt interaction based on input type
  if (isTouch) {
    // Enlarge touch targets
    element.classList.add('touch-friendly');
  }

  // Capture pointer to track movement outside element
  element.setPointerCapture(event.pointerId);
});

element.addEventListener('pointermove', event => {
  // Handle movement (drag operations, etc.)
});

element.addEventListener('pointerup', event => {
  // Release capture when interaction ends
  element.releasePointerCapture(event.pointerId);
});

This unified approach reduces code complexity while improving the experience across devices.

Creating Accessible Keyboard Navigation

True responsiveness includes adapting to different interaction methods. Keyboard navigation is essential for accessibility:

// Create a focus trap for modals and popups
function createFocusTrap(element) {
  // Find all focusable elements
  const focusableElements = element.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );

  const firstElement = focusableElements[0];
  const lastElement = focusableElements[focusableElements.length - 1];

  // Track focus and redirect as needed
  element.addEventListener('keydown', event => {
    if (event.key === 'Tab') {
      if (event.shiftKey && document.activeElement === firstElement) {
        // Shift+Tab on first element moves to last
        event.preventDefault();
        lastElement.focus();
      } else if (!event.shiftKey && document.activeElement === lastElement) {
        // Tab on last element moves to first
        event.preventDefault();
        firstElement.focus();
      }
    }
  });

  // Initially focus the first element
  firstElement.focus();
}

// Usage
const modal = document.querySelector('.modal');
createFocusTrap(modal);

Putting It All Together: A Self-Adaptive Card Component

Let's build a component that demonstrates these principles working together:

class ResponsiveCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // Initial render
    this.render();

    // Setup observers
    this.setupResizeObserver();
    this.setupPreferenceObservers();
    this.setupInteractionHandlers();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          background: var(--card-bg, white);
          color: var(--card-text, black);
          border-radius: 8px;
          padding: 20px;
          transition: transform 0.3s ease;
        }

        :host(.compact) .card-content {
          display: none;
        }

        :host(.compact) .card-title {
          font-size: 16px;
        }

        .card-title {
          font-size: 24px;
          margin-top: 0;
        }

        @media (prefers-reduced-motion: reduce) {
          :host {
            transition: none;
          }
        }
      </style>

      <h2 class="card-title">${this.getAttribute('title') || 'Card Title'}</h2>
      <div class="card-content">
        <slot></slot>
      </div>
    `;
  }

  setupResizeObserver() {
    this.resizeObserver = new ResizeObserver(entries => {
      for (const entry of entries) {
        const width = entry.contentRect.width;

        // Apply compact mode under 300px
        this.classList.toggle('compact', width < 300);

        // Dynamic typography scaling
        const titleElement = this.shadowRoot.querySelector('.card-title');
        if (titleElement) {
          // Scale font between 16px and 24px based on width
          const fontSize = Math.max(16, Math.min(24, width / 20));
          titleElement.style.fontSize = `${fontSize}px`;
        }
      }
    });

    this.resizeObserver.observe(this);
  }

  setupPreferenceObservers() {
    // Dark mode detection
    const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const updateTheme = (isDark) => {
      this.style.setProperty('--card-bg', isDark ? '#2a2a2a' : 'white');
      this.style.setProperty('--card-text', isDark ? 'white' : 'black');
    };

    updateTheme(darkModeQuery.matches);
    darkModeQuery.addEventListener('change', e => updateTheme(e.matches));
  }

  setupInteractionHandlers() {
    // Unified pointer handling
    this.addEventListener('pointerdown', e => {
      if (e.pointerType === 'touch') {
        // Larger touch feedback
        this.style.transform = 'scale(0.98)';
      }
    });

    this.addEventListener('pointerup', () => {
      this.style.transform = '';
    });

    // Keyboard support
    this.setAttribute('tabindex', '0');
    this.addEventListener('keydown', e => {
      if (e.key === 'Enter' || e.key === ' ') {
        // Trigger same action as click
        this.dispatchEvent(new CustomEvent('card-activate'));
      }
    });
  }

  disconnectedCallback() {
    // Clean up observers
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
    }
  }
}

// Register the custom element
customElements.define('responsive-card', ResponsiveCard);

Testing and Browser Compatibility

While these APIs offer powerful capabilities, browser support varies. Always implement feature detection:

// Feature detection for ResizeObserver
if ('ResizeObserver' in window) {
  // Proceed with ResizeObserver implementation
} else {
  // Fallback to alternative approach
  console.log('ResizeObserver not supported');
}

// Feature detection for Pointer Events
const supportsPointerEvents = 'PointerEvent' in window;

For comprehensive testing, consider:

  1. Visual regression testing across breakpoints

  2. Accessibility testing with tools like Lighthouse

  3. Performance monitoring especially on low-end devices

  4. Cross-browser testing particularly for newer APIs

Conclusion: A More Nuanced Approach to Responsiveness

By leveraging native browser APIs, we can create interfaces that respond not just to viewport dimensions, but to the full context of how our components are used:

  • ResizeObserver enables truly component-level responsiveness

  • MediaQueryList API helps adapt to user preferences and device capabilities

  • Pointer Events unify input handling across devices

  • Focus management ensures keyboard accessibility

These techniques represent a shift from the traditional "mobile-first" mentality toward a more nuanced "context-first" approach—one where components intelligently adapt to their specific circumstances rather than rigidly following global breakpoints.

As web applications grow increasingly complex, embracing these native capabilities becomes essential for creating interfaces that feel responsive in every sense of the word—not just adaptable to different screen sizes, but truly responsive to the people using them.


The native web platform continues to evolve with powerful APIs that reduce our dependence on heavy frameworks. By mastering these techniques, you'll build more efficient, accessible, and truly responsive interfaces that work for everyone.

0
Subscribe to my newsletter

Read articles from Mikey Nichols directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mikey Nichols
Mikey Nichols

I am an aspiring web developer on a mission to kick down the door into tech. Join me as I take the essential steps toward this goal and hopefully inspire others to do the same!