README.md
June 5, 2026 · View on GitHub
Signal-powered breakpoints & media queries for Angular
Documentation · Live demo · Why ngx-mq?
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-22
Angular 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.
API reference (quick view)
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
Sponsors
|
Sentry
|