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:
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const element = entry.target;
const width = entry.contentRect.width;
if (width < 400) {
element.classList.add('compact');
} else {
element.classList.remove('compact');
}
}
});
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;
let activeBreakpoint = null;
for (const [name, size] of Object.entries(breakpoints)) {
if (width <= size) {
activeBreakpoint = name;
break;
}
}
callback(activeBreakpoint, width);
}
});
observer.observe(element);
return observer;
}
const container = document.querySelector('.card-container');
createContainerQuery(
container,
{ small: 300, medium: 600, large: 900 },
(breakpoint, width) => {
container.dataset.size = breakpoint || 'xlarge';
}
);
This approach enables true component-level responsiveness without relying on global viewport conditions.
Beyond element dimensions, truly responsive interfaces adapt to user preferences. The MediaQueryList API brings the power of media queries to JavaScript:
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
function applyTheme(isDarkMode) {
document.documentElement.classList.toggle('dark-theme', isDarkMode);
}
applyTheme(darkModeQuery.matches);
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');
element.addEventListener('pointerdown', event => {
const isTouch = event.pointerType === 'touch';
const isPen = event.pointerType === 'pen';
const isMouse = event.pointerType === 'mouse';
if (isTouch) {
element.classList.add('touch-friendly');
}
element.setPointerCapture(event.pointerId);
});
element.addEventListener('pointermove', event => {
});
element.addEventListener('pointerup', event => {
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:
function createFocusTrap(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
element.addEventListener('keydown', event => {
if (event.key === 'Tab') {
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
});
firstElement.focus();
}
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' });
this.render();
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;
this.classList.toggle('compact', width < 300);
const titleElement = this.shadowRoot.querySelector('.card-title');
if (titleElement) {
const fontSize = Math.max(16, Math.min(24, width / 20));
titleElement.style.fontSize = `${fontSize}px`;
}
}
});
this.resizeObserver.observe(this);
}
setupPreferenceObservers() {
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() {
this.addEventListener('pointerdown', e => {
if (e.pointerType === 'touch') {
this.style.transform = 'scale(0.98)';
}
});
this.addEventListener('pointerup', () => {
this.style.transform = '';
});
this.setAttribute('tabindex', '0');
this.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
this.dispatchEvent(new CustomEvent('card-activate'));
}
});
}
disconnectedCallback() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
}
}
customElements.define('responsive-card', ResponsiveCard);
Testing and Browser Compatibility
While these APIs offer powerful capabilities, browser support varies. Always implement feature detection:
if ('ResizeObserver' in window) {
} else {
console.log('ResizeObserver not supported');
}
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.