ngx-mq
v3.1.0
Published
Signal-powered breakpoints and media queries for Angular. SSR-safe, zoneless-ready, no RxJS.
Maintainers
Readme
Overview
A responsive value is just a signal: read it in the template, compose it, and never wire up cleanup.
import { Component } from '@angular/core';
import { up } from 'ngx-mq';
@Component({
selector: 'app-root',
template: `
@if (isDesktop()) {
<app-sidebar />
}
`,
})
export class AppComponent {
readonly isDesktop = up('lg');
}- Signal-native so it works anywhere signals do, zoneless apps included.
- Zero boilerplate: no subscriptions, no
unsubscribe, cleanup is automatic. - SSR-safe with a value you control on the server.
- Batteries included: Tailwind, Bootstrap and Material presets, plus
and/or/not. - Tiny: ~1.9 kB gzipped, and no RxJS.
Install
npm i ngx-mq # Angular 20-22Angular 19 -> ngx-mq@2 · Angular 16-18 -> ngx-mq@1
Then register your breakpoints once, at bootstrap:
import { provideBreakpoints } from 'ngx-mq';
bootstrapApplication(AppComponent, {
providers: [provideBreakpoints({ sm: 640, md: 768, lg: 1024 })],
// or a preset: provideTailwindBreakpoints() / provideBootstrapBreakpoints() / provideMaterialBreakpoints()
});Call the helpers inside an injection context: a component field, a constructor, or a DI factory.
Examples
Show different layouts per screen size
readonly isMobile = down('md');
readonly isTablet = between('md', 'lg');
readonly isDesktop = up('lg');Follow the system dark mode
readonly prefersDark = colorScheme('dark');Drop hover styles on touch devices
// `hover()` has no direct inverse, so compose it
readonly isTouchLike = not(hover());Combine any conditions
// Large screen, in landscape, with a hover-capable pointer
readonly isLandscapeDesktop = and(up('lg'), orientation('landscape'), hover());
// Small screens OR a reduced-motion preference
readonly prefersSimpleUi = or(down('md'), reducedMotion());Respect reduced motion
readonly reduceMotion = reducedMotion();Anything else, with a raw query
readonly isRetina = matchMediaSignal('(min-resolution: 2dppx)');Why ngx-mq?
Angular's CDK ships BreakpointObserver, which works well but is built around RxJS and raw query strings. ngx-mq is built for the signals era: read a value in the template, subscribe to nothing, clean up automatically.
| | ngx-mq | CDK BreakpointObserver |
| --------------------- | ------------------------------------------- | ------------------------------------- |
| Reactivity | Signal<boolean> | Observable<BreakpointState> |
| Cleanup | Automatic via DestroyRef | Manual (takeUntilDestroyed) |
| Named breakpoints | Tailwind / Bootstrap / Material or your own | Material breakpoints or raw strings |
| Media-feature helpers | colorScheme, hover, pointer, ... | Raw query strings |
| Composition | and / or / not | RxJS operators |
| SSR | Configurable static value | Handle it yourself |
| Footprint | ~1.9 kB standalone | Part of @angular/cdk |
Documentation
Spin it up in seconds on StackBlitz, no setup required.
Full API reference, guides and recipes live at martsinlabs.github.io/ngx-mq.
Every query helper returns a Signal<boolean> and accepts an optional options argument
(CreateMediaQueryOptions).
Breakpoints
| Helper | Arguments | true when |
| --------- | ---------------- | ------------------------------------ |
| up | bp | viewport width >= bp |
| down | bp | viewport width < bp (exclusive) |
| between | minBp, maxBp | viewport width is in [minBp, maxBp) |
down and between upper bounds are exclusive: a small epsilon (default 0.02, set via provideBreakpointEpsilon) is subtracted from the max so adjacent ranges never overlap.
Media features
| Helper | Arguments | true when |
| --------------- | ------------------------------ | ---------------------------------------- |
| orientation | 'portrait' \| 'landscape' | the screen orientation matches |
| colorScheme | 'light' \| 'dark' | the system color scheme matches |
| displayMode | DisplayModeOption | the display mode matches (PWA detection) |
| reducedMotion | none | the user prefers reduced motion |
| prefersContrast | 'more' \| 'less' \| 'no-preference' \| 'custom' | the user's contrast preference matches |
| hover | none | the primary pointer can hover |
| anyHover | none | any available pointer can hover |
| pointer | 'fine' \| 'coarse' \| 'none' | the primary pointer matches |
| anyPointer | 'fine' \| 'coarse' \| 'none' | any available pointer matches |
| colorGamut | 'srgb' \| 'p3' \| 'rec2020' | the display covers the gamut |
Composition
| Helper | Arguments | true when |
| ------ | ---------------------------------- | ----------------------------------------- |
| and | ...conditions: Signal<boolean>[] | every condition is true (empty: true) |
| or | ...conditions: Signal<boolean>[] | any condition is true (empty: false) |
| not | condition: Signal<boolean> | the condition is false |
Custom queries
| Helper | Arguments | Description |
| ------------------ | --------------- | -------------------------------------------- |
| matchMediaSignal | query: string | A signal for any raw CSS media query |
Providers
| Provider | Argument | Description |
| ----------------------------- | -------------------- | ------------------------------------------------------- |
| provideBreakpoints | bps: MqBreakpoints | Registers a custom breakpoint map |
| provideTailwindBreakpoints | none | Registers the Tailwind preset |
| provideBootstrapBreakpoints | none | Registers the Bootstrap preset |
| provideMaterialBreakpoints | none | Registers the Material 2 preset |
| provideBreakpointEpsilon | epsilon: number | Sets the exclusive-bound epsilon (default 0.02) |
| provideSsrValue | value: boolean | Sets the value signals report during SSR (default false) |
Options and types
interface CreateMediaQueryOptions {
ssrValue?: boolean; // value reported during SSR; overrides provideSsrValue
debugName?: string; // shown for the signal in Angular DevTools
}
type MqBreakpoints = Record<string, number>;
type DisplayModeOption =
| 'browser' | 'fullscreen' | 'standalone'
| 'minimal-ui' | 'window-controls-overlay' | 'picture-in-picture';Server-side rendering
matchMedia does not exist on the server, so each signal returns a static value during SSR and switches to the live result after hydration. Set the default with provideSsrValue(true), or override per call with up('lg', { ssrValue: true }).
Contributing
Contributions are welcome. See CONTRIBUTING.md and ARCHITECTURE.md.
License
MIT © Martsin Labs
