match-media

Overview

Overview

Section titled Overview

The MatchMediaController is a reactive controller that binds a CSS media query to a reactive element, automatically updating when the query matches or stops matching. It leverages the match media API to query the state of CSS media queries from JavaScript while providing an event-based API to listen for changes.

Features

Section titled Features
  • Reactive media query monitoring: Automatically updates the host element when media query state changes
  • Event-driven: Listens for changes and triggers host updates
  • Multiple instances: Support multiple controllers on a single host for complex responsive layouts
  • Performance optimized: Uses native browser APIs for efficient media query observation

Usage

Section titled Usage

See it on NPM! How big is this package in your project?

yarn add @spectrum-web-components/reactive-controllers

Import the MatchMediaController via:

import { MatchMediaController } from '@spectrum-web-components/reactive-controllers/src/MatchMedia.js';

Examples

Section titled Examples

Basic usage

Section titled Basic usage

A Host element that renders different content based on window orientation:

import { html, LitElement } from 'lit';
import { MatchMediaController } from '@spectrum-web-components/reactive-controllers/src/MatchMedia.js';

class ResponsiveElement extends LitElement {
    orientationLandscape = new MatchMediaController(
        this,
        '(orientation: landscape)'
    );

    render() {
        if (this.orientationLandscape.matches) {
            return html`
                <p>The orientation is landscape.</p>
            `;
        }
        return html`
            <p>The orientation is portrait.</p>
        `;
    }
}

customElements.define('responsive-element', ResponsiveElement);

Multiple media queries

Section titled Multiple media queries

Use multiple MatchMediaController instances to create complex responsive layouts:

import { html, LitElement, css } from 'lit';
import { MatchMediaController } from '@spectrum-web-components/reactive-controllers/src/MatchMedia.js';

class ResponsiveLayout extends LitElement {
    isMobile = new MatchMediaController(this, '(max-width: 768px)');
    isTablet = new MatchMediaController(
        this,
        '(min-width: 769px) and (max-width: 1024px)'
    );
    isDesktop = new MatchMediaController(this, '(min-width: 1025px)');

    static styles = css`
        :host {
            display: block;
            padding: var(--spacing, 16px);
        }

        .mobile {
            font-size: 14px;
        }
        .tablet {
            font-size: 16px;
        }
        .desktop {
            font-size: 18px;
        }
    `;

    render() {
        const deviceClass = this.isMobile.matches
            ? 'mobile'
            : this.isTablet.matches
              ? 'tablet'
              : 'desktop';

        return html`
            <div
                class=${deviceClass}
                role="region"
                aria-label="Responsive content"
            >
                <h1>Current viewport: ${deviceClass}</h1>
                <p>Content adapts to your screen size.</p>
            </div>
        `;
    }
}

customElements.define('responsive-layout', ResponsiveLayout);

Dark mode detection

Section titled Dark mode detection

Detect and respond to user's color scheme preference:

import { html, LitElement, css } from 'lit';
import {
    MatchMediaController,
    DARK_MODE,
} from '@spectrum-web-components/reactive-controllers/src/MatchMedia.js';

class ThemeAwareComponent extends LitElement {
    darkMode = new MatchMediaController(this, DARK_MODE);

    static styles = css`
        :host {
            display: block;
            padding: 20px;
            transition:
                background-color 0.3s,
                color 0.3s;
        }

        .light-theme {
            background-color: #ffffff;
            color: #000000;
        }

        .dark-theme {
            background-color: #1a1a1a;
            color: #ffffff;
        }
    `;

    render() {
        const theme = this.darkMode.matches ? 'dark-theme' : 'light-theme';
        const themeLabel = this.darkMode.matches ? 'dark mode' : 'light mode';

        return html`
            <div
                class=${theme}
                role="region"
                aria-label="Theme-aware content in ${themeLabel}"
            >
                <h2>Current theme: ${themeLabel}</h2>
                <p>
                    This component automatically adapts to your system theme
                    preference.
                </p>
            </div>
        `;
    }
}

customElements.define('theme-aware-component', ThemeAwareComponent);

Mobile detection

Section titled Mobile detection

Detect mobile devices with touch input:

import { html, LitElement } from 'lit';
import {
    MatchMediaController,
    IS_MOBILE,
} from '@spectrum-web-components/reactive-controllers/src/MatchMedia.js';
import '@spectrum-web-components/button/sp-button.js';

class TouchOptimizedElement extends LitElement {
    isMobile = new MatchMediaController(this, IS_MOBILE);

    render() {
        const buttonSize = this.isMobile.matches ? 'xl' : 'm';
        const instructions = this.isMobile.matches
            ? 'Tap to continue'
            : 'Click to continue';

        return html`
            <div role="region" aria-label="Touch-optimized interface">
                <sp-button size=${buttonSize} aria-label="${instructions}">
                    ${instructions}
                </sp-button>
            </div>
        `;
    }
}

customElements.define('touch-optimized-element', TouchOptimizedElement);

Accessibility

Section titled Accessibility

When using MatchMediaController to create responsive designs, consider these accessibility best practices:

Content parity

Section titled Content parity
  • Ensure that content available on one screen size is also available on others, even if the presentation differs.
  • Don't hide critical information or functionality based solely on screen size.

Touch targets

Section titled Touch targets
  • On mobile devices (detected via media queries), ensure interactive elements meet the minimum touch target size of 44x44 pixels as per WCAG 2.5.5 Target Size.
  • Increase spacing between interactive elements on touch devices.

Responsive text

Section titled Responsive text
  • Ensure text remains readable at all breakpoints.
  • Allow text to reflow naturally without horizontal scrolling (required by WCAG 1.4.10 Reflow).

Keyboard navigation

Section titled Keyboard navigation
  • Responsive layouts must maintain logical keyboard navigation order.
  • Ensure focus indicators remain visible and clear at all breakpoints.

ARIA labels

Section titled ARIA labels
  • Update ARIA labels when content significantly changes based on media queries.
  • Use aria-label to describe the current layout state when it affects user interaction.

Screen reader announcements

Section titled Screen reader announcements
  • Consider using aria-live regions to announce significant layout changes.
  • Avoid disorienting users with unexpected content shifts.

Color scheme preferences

Section titled Color scheme preferences

When using DARK_MODE or other color scheme media queries:

  • Respect user preferences for reduced motion (prefers-reduced-motion).
  • Maintain sufficient contrast ratios in both light and dark modes.
  • Test with high contrast modes.

References

Section titled References
  • WCAG 2.1 - Reflow
  • WCAG 2.1 - Target Size
  • MDN: Using media queries for accessibility
  • Adobe Accessibility Guidelines

Resources

Section titled Resources
  • MDN: Window.matchMedia()
  • MDN: Using media queries
  • CSS Media Queries Level 5 - Specification