@sandlada/breakpoint
v0.0.1-20260831.a
Published
Breakpoint Observer — Responsive viewport/element breakpoint observer (MD3, RxJS, zero-framework)
Readme
@sandlada/breakpoint
Responsive viewport / element breakpoint observer — zero framework, pure functional, data-last, 100% RxJS.
A production-grade, framework-agnostic breakpoint evaluation and observation library for viewport and arbitrary DOM elements. Built with a pure functional, higher-order function, parameter-last architecture on rxjs@^7.8.x. Supports custom breakpoints, overlapping ranges, precise operator control, AND/OR composition, SSR safety, zero top-level side effects, and runtime element switching with fully symmetric width and height dimensions.
Features
- Zero framework — pure functional design; no class instances,
rxjsis the only runtime dependency - 100% reactive —
state$(shareReplay(1)),activeWidthBreakpoints$/activeHeightBreakpoints$(distinctUntilChanged+shareReplay), synchronoussnapshot - Data-last & currying —
matchesBreakpointDefinition(def)(widthPx),evaluateBreakpointMap(map)(widthPx) - Viewport & element — observe
windowby default or anyHTMLElementviaResizeObserver+rAFcoalescing; switch at runtime withattachElement() - MD3 defaults — width (
compact <600,medium 600–839,expanded 840–1199,large 1200–1599,extraLarge >=1600) and height (compact <480,medium 480–899,expanded >=900) with aliasesxs/sm/md/lg/xl - String operators —
>,>=,<,<=,=,==,!=(e.g.'> 840px','= 1200px','!= 960px'), whitespace-tolerant, decimal support,px/rem/emand absolute/viewport units - AND / OR composition —
{ and: [...] }/{ or: [...] }explicit composition - Overlapping ranges — each breakpoint evaluated independently; multiple active breakpoints simultaneously
- SSR safe — strict
typeof window === 'undefined'short-circuit; initial state configured viadefaultWidthMatches/defaultHeightMatches - Tree-shakable & zero top-level side-effects (
"sideEffects": false), ESM-only with unbundled module structure +d.ts
Install
# npm
npm install @sandlada/breakpoint rxjsLive Demos (GitHub Pages)
Explore interactive examples directly in your browser without local installation:
| Demo Scene | Description | Live Link |
| :--- | :--- | :--- |
| 1. Viewport MD3 Basics | Standard Material Design 3 viewport breakpoints (compact, medium, expanded, large, extraLarge) | 🌐 Open Live Demo |
| 2. Container Breakpoints | ResizeObserver-driven element container queries with resizable DOM box & runtime switching | 🌐 Open Live Demo |
| 3. Custom & Composition | Custom operators (>, <=, !=), rem/em units, and { and: [...] } / { or: [...] } logical composition | 🌐 Open Live Demo |
| 4. Height & 2D Observer | Symmetric viewport height breakpoints & 2D dual-dimension responsive metrics | 🌐 Open Live Demo |
| 5. RxJS Streams & Lifecycle | Cold observables (observeWidthBreakpoint), RxJS pipe operators, and auto-cleanup | 🌐 Open Live Demo |
Usages
Quick Start
🌐 Live Demo: Try MD3 Viewport Observer on GitHub Pages
import { createBreakpointObserver } from '@sandlada/breakpoint'
const observer = createBreakpointObserver()
// Subscribe to state stream
const subscription = observer.state$.subscribe((state) => {
console.log(state.activeWidthBreakpoints) // e.g. ['medium', 'sm']
console.log(state.widthMatches) // { compact: false, medium: true, ... }
})
// Read synchronous snapshot
console.log(observer.snapshot.activeWidthBreakpoints)
// Clean up when done
subscription.unsubscribe()
observer.dispose()Standalone Reactive Streams
🌐 Live Demo: Try RxJS Streams & Lifecycle on GitHub Pages
import {
observeBreakpointState,
observeWidthBreakpoint,
observeActiveWidthBreakpoints,
} from '@sandlada/breakpoint'
// Observe a single condition stream (cold observable, auto-cleans on unsubscription)
const isExpanded$ = observeWidthBreakpoint('>= 840px')
const sub1 = isExpanded$.subscribe((isExpanded) => {
console.log('isExpanded:', isExpanded)
})
// Observe active breakpoint keys stream
const activeKeys$ = observeActiveWidthBreakpoints()
const sub2 = activeKeys$.subscribe((keys) => {
console.log('active keys:', keys)
})
// Unsubscribe to clean up underlying listeners
sub1.unsubscribe()
sub2.unsubscribe()Pure Functional Evaluation & Currying (Data-Last)
All evaluation functions support direct invocation, curried (data-last) partial application, and argument reordering:
import {
matchesBreakpointCondition,
matchesBreakpointDefinition,
evaluateBreakpointMap,
DEFAULT_WIDTH_BREAKPOINTS,
} from '@sandlada/breakpoint'
// 1. Curried condition evaluation (data-last)
const isLargeWidth = matchesBreakpointCondition('>= 1200px')
console.log(isLargeWidth(1440)) // true
console.log(isLargeWidth(800)) // false
// 2. Direct condition evaluation
console.log(matchesBreakpointCondition('>= 1200px', 1440)) // true
console.log(matchesBreakpointCondition(1440, '>= 1200px')) // true
// 3. Definition evaluation with AND/OR logic
const isTablet = matchesBreakpointDefinition({ and: ['>= 600px', '< 1024px'] })
console.log(isTablet(768)) // true
// 4. Map evaluation
const evaluateMd3 = evaluateBreakpointMap(DEFAULT_WIDTH_BREAKPOINTS)
const result = evaluateMd3(1024)
console.log(result.activeBreakpoints) // ['expanded', 'md']
console.log(result.matchesTable) // { compact: false, medium: false, expanded: true, ... }Custom Breakpoints & Logical Composition
🌐 Live Demo: Try Custom Conditions & Composition on GitHub Pages
import { createBreakpointObserver, Breakpoint } from '@sandlada/breakpoint'
const observer = createBreakpointObserver({
widthBreakpoints: {
mobile: '< 600px',
tablet: Breakpoint.interval(600, 1024),
desktop: '>= 1024px',
customOr: { or: ['< 480px', '>= 1920px'] },
customAnd: { and: ['>= 768px', '<= 1440px'] },
customRange: { min: 600, max: 960, minInclusive: true, maxInclusive: false },
},
})Element Observation & Runtime Switching
🌐 Live Demo: Try Element Container Queries on GitHub Pages
import { createBreakpointObserver } from '@sandlada/breakpoint'
const containerElement = document.querySelector('#container') as HTMLElement
const containerObserver = createBreakpointObserver({
element: containerElement,
dimension: 'both', // 'width' | 'height' | 'both'
})
containerObserver.state$.subscribe((state) => {
console.log(state.width, state.height)
console.log(state.activeWidthBreakpoints, state.activeHeightBreakpoints)
})
// Switch observed element at runtime
const otherElement = document.querySelector('#sidebar') as HTMLElement
containerObserver.attachElement(otherElement)
// Detach element to fall back to viewport observation
containerObserver.detachElement()
// Destroy observer and disconnect ResizeObserver
containerObserver.dispose()Server-Side Rendering (SSR)
The library safely evaluates on Node.js/SSR environments without window errors:
import { createBreakpointObserver, isServer } from '@sandlada/breakpoint'
console.log(isServer()) // true on server
const ssrObserver = createBreakpointObserver({
defaultWidthMatches: {
compact: true,
medium: false,
expanded: false,
},
})
// Synchronous snapshot and initial stream values use defaults
console.log(ssrObserver.snapshot.activeWidthBreakpoints) // ['compact']API Reference
Observer & Stream Creators
createBreakpointObserver(configuration?: BreakpointConfiguration): BreakpointObserverInstanceobserveBreakpointState(configuration?: BreakpointConfiguration): Observable<BreakpointState>observeBreakpoint(definition: BreakpointDefinition, dimension?: BreakpointDimension, configuration?: BreakpointConfiguration): Observable<boolean>observeWidthBreakpoint(definition: BreakpointDefinition, configuration?: BreakpointConfiguration): Observable<boolean>observeHeightBreakpoint(definition: BreakpointDefinition, configuration?: BreakpointConfiguration): Observable<boolean>observeActiveBreakpoints(dimension?: BreakpointDimension, configuration?: BreakpointConfiguration): Observable<string[]>observeActiveWidthBreakpoints(configuration?: BreakpointConfiguration): Observable<string[]>observeActiveHeightBreakpoints(configuration?: BreakpointConfiguration): Observable<string[]>getDefaultViewportObserver(): BreakpointObserverInstance
Pure Evaluation & Transformation Functions
parseBreakpointCondition(conditionString: BreakpointCondition): ParsedBreakpointConditionmatchesBreakpointCondition(condition: BreakpointCondition, options?: BreakpointEvaluationOptions): (value: number) => booleanmatchesBreakpointCondition(condition: BreakpointCondition, value: number, options?: BreakpointEvaluationOptions): booleanmatchesBreakpointCondition(value: number, condition: BreakpointCondition, options?: BreakpointEvaluationOptions): booleanmatchesBreakpointDefinition(definition: BreakpointDefinition, options?: BreakpointEvaluationOptions): (value: number) => booleanmatchesBreakpointDefinition(definition: BreakpointDefinition, value: number, options?: BreakpointEvaluationOptions): booleanmatchesBreakpointDefinition(value: number, definition: BreakpointDefinition, options?: BreakpointEvaluationOptions): booleanevaluateBreakpointMap(map: BreakpointMap, options?: BreakpointEvaluationOptions): (value: number) => BreakpointEvaluationResultevaluateBreakpointMap(map: BreakpointMap, value: number, options?: BreakpointEvaluationOptions): BreakpointEvaluationResultevaluateBreakpointMap(value: number, map: BreakpointMap, options?: BreakpointEvaluationOptions): BreakpointEvaluationResultcomputeBreakpointState(targetWidthPx: number, targetHeightPx: number, configuration?: BreakpointConfiguration): BreakpointStateconvertConditionToMediaQuery(condition: BreakpointCondition, dimension: 'width' | 'height', mediaQueryExclusiveStep: number): string | nullconvertDefinitionToMediaQuery(definition: BreakpointDefinition, dimension: 'width' | 'height', mediaQueryExclusiveStep: number): string | null
Breakpoint Builders & Helpers
Breakpoint.gt(value: number, unit?: BreakpointUnit): BreakpointConditionBreakpoint.gte(value: number, unit?: BreakpointUnit): BreakpointConditionBreakpoint.lt(value: number, unit?: BreakpointUnit): BreakpointConditionBreakpoint.lte(value: number, unit?: BreakpointUnit): BreakpointConditionBreakpoint.eq(value: number, unit?: BreakpointUnit): BreakpointConditionBreakpoint.ne(value: number, unit?: BreakpointUnit): BreakpointConditionBreakpoint.interval(min: number, max: number, options?: BreakpointIntervalOptions): BreakpointDefinitionBreakpoint.between(min: number, max: number, options?: BreakpointIntervalOptions): BreakpointDefinitiongreaterThan(value: number, unit?: BreakpointUnit): BreakpointConditiongreaterThanOrEqual(value: number, unit?: BreakpointUnit): BreakpointConditionlessThan(value: number, unit?: BreakpointUnit): BreakpointConditionlessThanOrEqual(value: number, unit?: BreakpointUnit): BreakpointConditionequals(value: number, unit?: BreakpointUnit): BreakpointConditionnotEquals(value: number, unit?: BreakpointUnit): BreakpointConditioncreateBreakpointInterval(min: number, max: number, options?: BreakpointIntervalOptions): BreakpointDefinition
Environment & SSR Utilities
isServer(): booleanisBrowser(): booleancanUseDOM(): booleancanUseMatchMedia(): booleancanUseResizeObserver(): booleancanUseRequestAnimationFrame(): booleangetWindow(): (Window & typeof globalThis) | undefinedgetDocument(): Document | undefined
Reactive & Comparison Utilities
Subject(re-exported fromrxjs)isShallowEqualArray<T>(firstArray: readonly T[], secondArray: readonly T[]): booleanisShallowEqualRecord(firstRecord: Readonly<Record<string, boolean>>, secondRecord: Readonly<Record<string, boolean>>): boolean
Constants
DEFAULT_WIDTH_BREAKPOINTS: Readonly<WidthBreakpointMap>DEFAULT_HEIGHT_BREAKPOINTS: Readonly<HeightBreakpointMap>ABSOLUTE_PX: Readonly<Record<AbsoluteBreakpointUnit, number>>REM_BASE: number(16)EM_BASE: number(16)
Types & Interfaces
BreakpointObserverInstanceBreakpointStateBreakpointConfigurationBreakpointDefinitionBreakpointConditionBreakpointRangeBreakpointDimension('width' | 'height' | 'both')BreakpointLogic('and' | 'or')BreakpointUnit/ParsedBreakpointUnitAbsoluteBreakpointUnitWidthBreakpointMap/HeightBreakpointMap/BreakpointMapBreakpointEvaluationOptionsBreakpointEvaluationResultBreakpointIntervalOptionsParsedBreakpointCondition
Testing & Quality Assurance
This repository employs a Spec-First / Black-Box testing methodology. Tests verify interface signatures, mathematical boundary constraints, and lifecycle contracts without coupling to internal implementations.
Each test suite organizes specifications with structured nested describe blocks:
Happy Path: Standard valid inputs, mathematical interval correctness, state calculations, and currying equivalence.Boundary & Error Handling: Critical boundary transitions (±1px, 0px, float precision), malformed syntax exceptions (TypeError), and empty or unsupported unit fallbacks.RxJS Streams & Teardown: Cold observable verification, initial synchronous emissions, multi-subscriber multicast,distinctUntilChangeddeduplication, and complete listener release uponunsubscribe()/dispose().Environment Isolation: SSR safety (isServer: true), customdefaultWidthMatches/defaultHeightMatchesfallback handling, and graceful degradation when browser APIs are unavailable.
npm run lint:types # Typecheck with tsc --noEmit
npm test # Run vitest test suite
npm run build # Build dist via tsdownLicense
MIT
