Beyond Media Queries


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:
Components lack context awareness - A sidebar component doesn't know if it's in a narrow dashboard or a wide homepage
Nested components can't respond independently - Elements within containers can't easily adapt to their immediate parent's dimensions
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 modeprefers-reduced-motion
for animation sensitivityprefers-contrast
for accessibility needsprefers-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:
Visual regression testing across breakpoints
Accessibility testing with tools like Lighthouse
Performance monitoring especially on low-end devices
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.
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!