responsive-media 1.0.7 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,141 +1,877 @@
1
- npm install responsive-media
2
-
3
- # responsive-media
4
-
5
- A utility for reactive state based on CSS media queries. Includes integration with Vue 3 (Composition API).
6
-
7
- ## Installation
8
-
9
- ```
10
- npm install responsive-media
11
- ```
12
-
13
- ## Usage without Vue
14
-
15
- ### Getting the state
16
-
17
- ```ts
18
- import { responsiveState } from 'responsive-media';
19
-
20
- // Get the current state:
21
- const { mobile, tablet, desktop } = responsiveState.proxy;
22
-
23
- console.log('isMobile:', mobile);
24
- console.log('isTablet:', tablet);
25
- console.log('isDesktop:', desktop);
26
- ```
27
-
28
- ### Subscribing to changes
29
-
30
- ```ts
31
- // Subscribe to state changes:
32
- const unsubscribe = responsiveState.subscribe((state) => {
33
- console.log('State changed:', state);
34
- // state.mobile, state.tablet, state.desktop
35
- });
36
-
37
- // To unsubscribe:
38
- unsubscribe();
39
- ```
40
-
41
- ## Usage with Vue 3 (Composition API)
42
-
43
- ### Plugin registration
44
-
45
- ```ts
46
- import { createApp } from 'vue';
47
- import { ResponsivePlugin, ResponsiveConfig } from 'responsive-media';
48
- import App from './App.vue';
49
-
50
- const app = createApp(App);
51
-
52
- // Use default breakpoints:
53
- app.use(ResponsivePlugin);
54
-
55
- // Or provide your own breakpoints (now you can combine conditions):
56
- app.use(ResponsivePlugin, {
57
- ...ResponsiveConfig, // keep default breakpoints
58
- myCustom: [
59
- { type: 'min-width', value: 1200 },
60
- { type: 'aspect-ratio', value: '16/9' },
61
- ], // add your own with multiple conditions
62
- mobile: [
63
- { type: 'max-width', value: 500 },
64
- { type: 'orientation', value: 'portrait' },
65
- ], // override default with multiple conditions
66
- });
67
-
68
- app.mount('#app');
69
- ```
70
-
71
- ### Usage in a component
72
-
73
- ```ts
74
- <script setup lang="ts">
75
- import { useResponsive } from 'responsive-media';
76
-
77
- const responsive = useResponsive();
78
-
79
- // responsive.mobile, responsive.tablet, responsive.desktop, etc.
80
- </script>
81
- ```
82
-
83
- ## Customizing breakpoints
84
-
85
- You can override or add your own breakpoints using the setResponsiveConfig function. Now each breakpoint can be an array of conditions (they will be combined with and):
86
-
87
- ```ts
88
- import { setResponsiveConfig, ResponsiveConfig } from 'responsive-media';
89
-
90
- setResponsiveConfig({
91
- ...ResponsiveConfig, // keep default
92
- myCustom: [
93
- { type: 'min-width', value: 1200 },
94
- { type: 'aspect-ratio', value: '16/9' },
95
- ], // add your own breakpoint with multiple conditions
96
- mobile: [
97
- { type: 'max-width', value: 500 },
98
- { type: 'orientation', value: 'portrait' },
99
- ], // override default with multiple conditions
100
- });
101
- ```
102
-
103
-
104
- ## Getting CSS media query strings
105
-
106
- You can get the generated CSS media query strings for each breakpoint:
107
-
108
- ```ts
109
- import { getResponsiveMediaQueries } from 'responsive-media';
110
-
111
- const mediaQueries = getResponsiveMediaQueries();
112
- console.log(mediaQueries);
113
- // {
114
- // mobile: '(max-width: 767px)',
115
- // tablet: '(min-width: 768px) and (max-width: 1023px)',
116
- // desktop: '(min-width: 1024px)'
117
- // }
118
- ```
119
-
120
- ## Exported entities
121
- - responsiveState
122
- - ResponsiveConfig
123
- - setResponsiveConfig
124
- - ResponsivePlugin (Vue)
125
- - useResponsive (Vue)
126
- - getResponsiveMediaQueries
127
-
128
- ## Breakpoint config format
129
-
130
- Each breakpoint is now described by an array of conditions (MediaQueryConfig = MediaQueryCondition[]), where each condition is an object with type and value. All conditions within a breakpoint are combined with and (e.g., `(max-width: 600px) and (aspect-ratio: 16/9)`).
131
-
132
- ## Author
133
-
134
- Danil Lisin Vladimirovich aka Macrulez
135
-
136
- GitHub: [macrulezru](https://github.com/macrulezru)
137
-
138
- Website: [macrulez.ru](https://macrulez.ru/)
139
-
140
- ## License
141
- MIT
1
+ <div align="center" style="background:#111827;border-radius:20px;padding:28px 20px 20px;margin-bottom:32px">
2
+ <h1 style="color:#f9fafb;margin:0 0 32px;font-size:2.2em;letter-spacing:-0.03em;font-weight:700;font-family:sans-serif">
3
+ responsive-media
4
+ </h1>
5
+ <img
6
+ src="https://s3.twcstorage.ru/c9a2cc89-780f97fd-311d-4a1a-b86f-c25665c9dc46/images/npm/responsive-media.webp"
7
+ alt="vue-virtual-scroller-kit"
8
+ style="max-width:100%;width:auto;height:300px;border-radius:12px"
9
+ />
10
+ </div>
11
+
12
+ A utility for creating reactive boolean state from CSS media queries and element dimensions. Useful when you need more than CSS — when you want to **imperatively react to viewport or container changes** in JavaScript.
13
+
14
+ - **Viewport breakpoints** — backed by `window.matchMedia`
15
+ - **Container queries** — backed by `ResizeObserver` (JS-side evaluation)
16
+ - **Vue 3** and **React 18+** adapters included
17
+ - **SSR-safe** — falls back to `false` on the server
18
+ - **Framework-agnostic** core works with Vanilla JS, signals libraries, or any other framework
19
+
20
+ ## Installation
21
+
22
+ ```
23
+ npm install responsive-media
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Table of Contents
29
+
30
+ 1. [Quick Start](#quick-start)
31
+ 2. [Config format MediaQueryConfig](#config-format--mediaqueryconfig)
32
+ 3. [Global singleton](#global-singleton)
33
+ 4. [createResponsiveState — isolated instances](#createresponsivestate--isolated-instances)
34
+ 5. [ContainerState — element container queries](#containerstate--element-container-queries)
35
+ 6. [Subscription API](#subscription-api)
36
+ 7. [Ordered breakpoint helpers](#ordered-breakpoint-helpers)
37
+ 8. [Utilities](#utilities)
38
+ 9. [Presets](#presets)
39
+ 10. [Vue 3 integration](#vue-3-integration)
40
+ 11. [React 18+ integration](#react-18-integration)
41
+ 12. [TypeScript helpers](#typescript-helpers)
42
+ 13. [SSR / hydration](#ssr--hydration)
43
+ 14. [Exported API reference](#exported-api-reference)
44
+
45
+ ---
46
+
47
+ ## Quick Start
48
+
49
+ ```ts
50
+ import { responsiveState, setResponsiveConfig } from 'responsive-media';
51
+
52
+ setResponsiveConfig({
53
+ mobile: [{ type: 'max-width', value: 767 }],
54
+ tablet: [{ type: 'min-width', value: 768 }, { type: 'max-width', value: 1023 }],
55
+ desktop: [{ type: 'min-width', value: 1024 }],
56
+ });
57
+
58
+ // Read current state
59
+ console.log(responsiveState.proxy.mobile); // true / false
60
+
61
+ // Subscribe to changes
62
+ const stop = responsiveState.subscribe((state) => {
63
+ console.log('desktop:', state.desktop);
64
+ });
65
+
66
+ // Cleanup
67
+ stop();
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Config format — MediaQueryConfig
73
+
74
+ Each breakpoint is described by a `MediaQueryConfig` — an array of conditions that are combined with **AND**, or a nested array of groups combined with **OR**.
75
+
76
+ ### AND (flat array)
77
+
78
+ ```ts
79
+ // (min-width: 768px) and (max-width: 1023px)
80
+ [
81
+ { type: 'min-width', value: 768 },
82
+ { type: 'max-width', value: 1023 },
83
+ ]
84
+ ```
85
+
86
+ ### OR (nested array)
87
+
88
+ ```ts
89
+ // (max-width: 600px), (orientation: portrait) and (max-width: 1024px)
90
+ [
91
+ [{ type: 'max-width', value: 600 }],
92
+ [{ type: 'orientation', value: 'portrait' }, { type: 'max-width', value: 1024 }],
93
+ ]
94
+ ```
95
+
96
+ ### Raw media type
97
+
98
+ Use `type: 'raw'` to insert a value verbatim — useful for media types like `print` or `screen`:
99
+
100
+ ```ts
101
+ // Matches 'print' media type
102
+ [{ type: 'raw', value: 'print' }]
103
+
104
+ // screen and (max-width: 600px)
105
+ [{ type: 'raw', value: 'screen' }, { type: 'max-width', value: 600 }]
106
+ ```
107
+
108
+ ### Supported condition types
109
+
110
+ | `type` | Example value | Generated query |
111
+ |-------------------------|-------------------|-----------------------------------|
112
+ | `min-width` | `768` | `(min-width: 768px)` |
113
+ | `max-width` | `1023` | `(max-width: 1023px)` |
114
+ | `min-height` | `600` | `(min-height: 600px)` |
115
+ | `max-height` | `900` | `(max-height: 900px)` |
116
+ | `orientation` | `'portrait'` | `(orientation: portrait)` |
117
+ | `aspect-ratio` | `'16/9'` | `(aspect-ratio: 16/9)` |
118
+ | `prefers-color-scheme` | `'dark'` | `(prefers-color-scheme: dark)` |
119
+ | `prefers-reduced-motion`| `'reduce'` | `(prefers-reduced-motion: reduce)`|
120
+ | `prefers-contrast` | `'more'` | `(prefers-contrast: more)` |
121
+ | `hover` | `'none'` | `(hover: none)` |
122
+ | `pointer` | `'coarse'` | `(pointer: coarse)` |
123
+ | `forced-colors` | `'active'` | `(forced-colors: active)` |
124
+ | `resolution` | `'2dppx'` | `(resolution: 2dppx)` |
125
+ | `display-mode` | `'standalone'` | `(display-mode: standalone)` |
126
+ | `raw` | `'print'` | `print` *(verbatim)* |
127
+ | … and more | | |
128
+
129
+ ---
130
+
131
+ ## Global singleton
132
+
133
+ The library exports a pre-configured singleton `responsiveState` initialized with the default `ResponsiveConfig` (mobile / tablet / desktop).
134
+
135
+ ```ts
136
+ import { responsiveState, setResponsiveConfig, getResponsiveMediaQueries } from 'responsive-media';
137
+
138
+ // Re-configure the singleton
139
+ setResponsiveConfig(
140
+ {
141
+ sm: [{ type: 'max-width', value: 767 }],
142
+ lg: [{ type: 'min-width', value: 1024 }],
143
+ },
144
+ {
145
+ order: ['sm', 'lg'], // for isAbove/isBelow/between
146
+ debounce: 50, // ms, throttle subscribe() listeners
147
+ }
148
+ );
149
+
150
+ // Read the current state snapshot
151
+ const { sm, lg } = responsiveState.getState();
152
+
153
+ // Direct proxy access (live, non-debounced)
154
+ console.log(responsiveState.proxy.sm);
155
+
156
+ // Get the generated CSS strings
157
+ const mq = getResponsiveMediaQueries();
158
+ // { sm: '(max-width: 767px)', lg: '(min-width: 1024px)' }
159
+ ```
160
+
161
+ ### Default config (`ResponsiveConfig`)
162
+
163
+ | Key | Range |
164
+ |-----------|--------------|
165
+ | `mobile` | ≤ 600px |
166
+ | `tablet` | 601 – 960px |
167
+ | `desktop` | ≥ 961px |
168
+
169
+ ---
170
+
171
+ ## createResponsiveState — isolated instances
172
+
173
+ Create independent instances — useful for per-request SSR, multiple independent contexts, or testing:
174
+
175
+ ```ts
176
+ import { createResponsiveState, TailwindPreset, TailwindOrder } from 'responsive-media';
177
+
178
+ const layoutState = createResponsiveState(TailwindPreset, {
179
+ order: [...TailwindOrder],
180
+ });
181
+
182
+ const themeState = createResponsiveState({
183
+ dark: [{ type: 'prefers-color-scheme', value: 'dark' }],
184
+ reducedMotion: [{ type: 'prefers-reduced-motion', value: 'reduce' }],
185
+ });
186
+
187
+ layoutState.subscribe((s) => console.log('layout:', s));
188
+ themeState.subscribe((s) => console.log('theme:', s));
189
+
190
+ // Cleanup when done (e.g. per-request SSR)
191
+ layoutState.destroy();
192
+ ```
193
+
194
+ ---
195
+
196
+ ## ContainerState — element container queries
197
+
198
+ `ContainerState` tracks an **element's dimensions** via `ResizeObserver` and evaluates breakpoint conditions in JavaScript — the same concept as CSS Container Queries, but in JS.
199
+
200
+ The API is identical to `ReactiveResponsiveState` — all subscription methods work the same way.
201
+
202
+ ```ts
203
+ import { createContainerState } from 'responsive-media/container';
204
+ // or: import { createContainerState } from 'responsive-media';
205
+
206
+ const card = document.querySelector('.card')!;
207
+
208
+ const cardState = createContainerState(card, {
209
+ compact: [{ type: 'max-width', value: 300 }],
210
+ normal: [{ type: 'min-width', value: 301 }, { type: 'max-width', value: 599 }],
211
+ wide: [{ type: 'min-width', value: 600 }],
212
+ }, {
213
+ order: ['compact', 'normal', 'wide'],
214
+ });
215
+
216
+ // Reactive class toggling
217
+ cardState.on('compact', (v) => card.classList.toggle('card--compact', v));
218
+
219
+ // Sync CSS custom properties: --card-compact: 1; --card-wide: 0; …
220
+ cardState.syncCSSVars({ prefix: '--card-' });
221
+
222
+ // Get @container-compatible query strings
223
+ const strings = cardState.getMediaQueries();
224
+ // { compact: '(max-width: 300px)', wide: '(min-width: 600px)' }
225
+
226
+ // Cleanup
227
+ cardState.destroy();
228
+ ```
229
+
230
+ ### Supported condition types for ContainerState
231
+
232
+ `max-width`, `min-width`, `max-height`, `min-height`, `orientation`, `aspect-ratio`
233
+
234
+ ---
235
+
236
+ ## Subscription API
237
+
238
+ All methods below are available on both `ReactiveResponsiveState` and `ContainerState`.
239
+
240
+ ### `subscribe(listener)` → unsubscribe
241
+
242
+ Fires immediately with the current state, then on every change. Affected by `debounce`.
243
+
244
+ ```ts
245
+ const stop = state.subscribe((s) => {
246
+ document.body.dataset.bp = Object.keys(s).filter(k => s[k]).join(' ');
247
+ });
248
+ stop(); // unsubscribe
249
+ ```
250
+
251
+ ### `on(key, callback)` → unsubscribe
252
+
253
+ Fires immediately with the current value for `key`, then on every change. **Never debounced.**
254
+
255
+ ```ts
256
+ const off = state.on('mobile', (matches) => {
257
+ header.classList.toggle('header--mobile', matches);
258
+ });
259
+ off();
260
+ ```
261
+
262
+ ### `onEnter(key, callback)` → unsubscribe
263
+
264
+ Fires only on `false → true` transitions. Skips the initial value. **Never debounced.**
265
+
266
+ ```ts
267
+ state.onEnter('mobile', () => initMobileMenu());
268
+ ```
269
+
270
+ ### `onLeave(key, callback)` → unsubscribe
271
+
272
+ Fires only on `true → false` transitions. Skips the initial value. **Never debounced.**
273
+
274
+ ```ts
275
+ state.onLeave('mobile', () => destroyMobileMenu());
276
+ ```
277
+
278
+ ### `once(key, callback)` → unsubscribe
279
+
280
+ Fires on the **next change** to `key`, then auto-unsubscribes. Does not fire for the current value. **Never debounced.**
281
+
282
+ ```ts
283
+ state.once('mobile', (matches) => {
284
+ console.log('mobile changed to:', matches);
285
+ });
286
+ ```
287
+
288
+ ### `onNextChange(callback)` → unsubscribe
289
+
290
+ Fires on the **next global state change**, then auto-unsubscribes. Affected by `debounce`.
291
+
292
+ ```ts
293
+ state.onNextChange((s) => console.log('first change:', s));
294
+ ```
295
+
296
+ ### `onBreakpointChange(callback)` → unsubscribe
297
+
298
+ Fires when the **active breakpoint** changes (i.e. `current` changes), providing `from` and `to`. Affected by `debounce`.
299
+
300
+ ```ts
301
+ state.onBreakpointChange((from, to) => {
302
+ console.log(`breakpoint: ${from} → ${to}`);
303
+ });
304
+ ```
305
+
306
+ ### `waitFor(key, expectedValue?)` → Promise
307
+
308
+ Returns a `Promise` that resolves when `key` reaches `expectedValue` (default `true`). Resolves immediately if already met. **Never debounced.**
309
+
310
+ ```ts
311
+ await state.waitFor('desktop');
312
+ initDesktopChart();
313
+
314
+ // Wait for mobile to become false
315
+ await state.waitFor('mobile', false);
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Ordered breakpoint helpers
321
+
322
+ These helpers require a breakpoint `order` — either set via `setConfig` / `createResponsiveState` options, or derived from config key insertion order.
323
+
324
+ ### `state.current` — getter
325
+
326
+ Returns the first active breakpoint key in order, or `null`.
327
+
328
+ ```ts
329
+ if (state.current === 'mobile') showDrawer();
330
+ ```
331
+
332
+ ### `state.isAbove(key)` → boolean
333
+
334
+ `true` when the current breakpoint comes **after** `key` in the order.
335
+
336
+ ```ts
337
+ // order: ['xs', 'sm', 'md', 'lg', 'xl']
338
+ // current = 'lg'
339
+ state.isAbove('sm') // → true
340
+ state.isAbove('xl') // → false
341
+ ```
342
+
343
+ ### `state.isBelow(key)` → boolean
344
+
345
+ `true` when the current breakpoint comes **before** `key` in the order.
346
+
347
+ ```ts
348
+ state.isBelow('md') // → true (current = 'sm')
349
+ ```
350
+
351
+ ### `state.between(from, to)` → boolean
352
+
353
+ `true` when the current breakpoint is between `from` and `to` (inclusive).
354
+
355
+ ```ts
356
+ state.between('sm', 'lg') // → true (current = 'md')
357
+ ```
358
+
359
+ ---
360
+
361
+ ## Utilities
362
+
363
+ ### `syncCSSVars(options?)` → stop
364
+
365
+ Syncs all breakpoint keys to CSS custom properties (`1` / `0`) on `document.documentElement` (or a custom element). Automatically removes properties for keys removed during a config change.
366
+
367
+ ```ts
368
+ const stop = state.syncCSSVars({ element: document.body, prefix: '--bp-' });
369
+ // → --bp-mobile: 1; --bp-desktop: 0; …
370
+ stop(); // cleanup
371
+ ```
372
+
373
+ **Options:**
374
+
375
+ | Option | Default | Description |
376
+ |-----------|--------------------------|----------------------------------|
377
+ | `element` | `document.documentElement` | Target HTML element |
378
+ | `prefix` | `'--responsive-'` | CSS custom property name prefix |
379
+
380
+ ### `emitDOMEvents(target?, options?)` → stop
381
+
382
+ Dispatches DOM `CustomEvent`s on `target` whenever breakpoints change:
383
+
384
+ - `responsive:change` — fires on any state change; `event.detail` is the full state snapshot
385
+ - `responsive:mobile:enter` — fires when `mobile` becomes `true`
386
+ - `responsive:mobile:leave` — fires when `mobile` becomes `false`
387
+
388
+ ```ts
389
+ const stop = state.emitDOMEvents(document, { prefix: 'bp:' });
390
+
391
+ document.addEventListener('bp:change', (e) => console.log(e.detail));
392
+ document.addEventListener('bp:mobile:enter', () => initDrawer());
393
+ document.addEventListener('bp:desktop:leave', () => destroyDesktopChart());
394
+
395
+ stop();
396
+ ```
397
+
398
+ **Options:**
399
+
400
+ | Option | Default | Description |
401
+ |----------|-----------------|--------------------------|
402
+ | `prefix` | `'responsive:'` | Custom event name prefix |
403
+
404
+ ### `toSignal(key, factory)` → Signal
405
+
406
+ Binds a breakpoint key to a writable signal from any signals library. The signal is kept in sync via `on()`.
407
+
408
+ ```ts
409
+ // @preact/signals-core
410
+ import { signal } from '@preact/signals-core';
411
+ const isMobile = state.toSignal('mobile', signal);
412
+ isMobile.value; // reactive boolean
413
+
414
+ // Angular signal
415
+ import { signal } from '@angular/core';
416
+ const isMobile = state.toSignal('mobile', signal);
417
+
418
+ // Vue ref
419
+ import { ref } from 'vue';
420
+ const isMobile = state.toSignal('mobile', ref);
421
+ ```
422
+
423
+ ### `getMediaQueries()` → Record\<string, string\>
424
+
425
+ Returns the generated CSS media query strings for each breakpoint key.
426
+
427
+ ```ts
428
+ const mq = state.getMediaQueries();
429
+ // { mobile: '(max-width: 600px)', desktop: '(min-width: 961px)' }
430
+ ```
431
+
432
+ ### `getState<T>()` → T
433
+
434
+ Returns a stable snapshot of the current state. Same reference between changes — safe for React's `useSyncExternalStore`.
435
+
436
+ ### `getOrder()` → string[]
437
+
438
+ Returns the configured breakpoint order array (or empty array if not set).
439
+
440
+ ### `hydrate(initialState)` — SSR hydration
441
+
442
+ Sets initial state from a server-side snapshot to prevent layout shift. Only updates keys that exist in the current config.
443
+
444
+ ```ts
445
+ // On the server, serialize state and pass to the client:
446
+ state.hydrate({ mobile: false, tablet: false, desktop: true });
447
+ ```
448
+
449
+ ### `destroy()`
450
+
451
+ Removes all `matchMedia` / `ResizeObserver` listeners, clears all subscribers, and cancels any pending debounce timer.
452
+
453
+ ### `toMediaQueryString(conditions)` — standalone utility
454
+
455
+ Converts a `MediaQueryConfig` to a CSS media query string. Useful for CSS-in-JS or debugging.
456
+
457
+ ```ts
458
+ import { toMediaQueryString } from 'responsive-media';
459
+
460
+ toMediaQueryString([{ type: 'min-width', value: 768 }, { type: 'max-width', value: 1024 }])
461
+ // → "(min-width: 768px) and (max-width: 1024px)"
462
+
463
+ toMediaQueryString([[{ type: 'max-width', value: 600 }], [{ type: 'orientation', value: 'portrait' }]])
464
+ // → "(max-width: 600px), (orientation: portrait)"
465
+ ```
466
+
467
+ ### `match(state, map, fallback?)` — standalone utility
468
+
469
+ Returns the first value in `map` whose key is `true` in `state`. Priority follows `map` insertion order.
470
+
471
+ ```ts
472
+ import { match } from 'responsive-media';
473
+ import { responsiveState } from 'responsive-media';
474
+
475
+ const cols = match(responsiveState.proxy, { mobile: 1, tablet: 2, desktop: 4 });
476
+ const View = match(responsiveState.proxy, { mobile: MobileMenu, desktop: DesktopNav });
477
+ const label = match(responsiveState.proxy, { sm: 'Compact', lg: 'Full' }, 'Default');
478
+ ```
479
+
480
+ ### `subscribeMediaQuery(query, callback)` — standalone utility
481
+
482
+ Low-level reactive wrapper around a single raw CSS media query string. Framework-agnostic — the Vue and React adapters use this internally.
483
+
484
+ ```ts
485
+ import { subscribeMediaQuery } from 'responsive-media';
486
+
487
+ const off = subscribeMediaQuery('(prefers-color-scheme: dark)', (matches) => {
488
+ document.body.classList.toggle('dark', matches);
489
+ });
490
+ off(); // cleanup
491
+ ```
492
+
493
+ ---
494
+
495
+ ## Presets
496
+
497
+ Import from `responsive-media/presets` or from the main entry point.
498
+
499
+ ### `ResponsiveConfig` (default)
500
+
501
+ | Key | Range |
502
+ |-----------|--------------|
503
+ | `mobile` | ≤ 600px |
504
+ | `tablet` | 601 – 960px |
505
+ | `desktop` | ≥ 961px |
506
+
507
+ ### `TailwindPreset` + `TailwindOrder`
508
+
509
+ Mutually exclusive Tailwind CSS v3/v4 breakpoints:
510
+
511
+ | Key | Range |
512
+ |-------|----------------|
513
+ | `xs` | ≤ 639px |
514
+ | `sm` | 640 – 767px |
515
+ | `md` | 768 – 1023px |
516
+ | `lg` | 1024 – 1279px |
517
+ | `xl` | 1280 – 1535px |
518
+ | `2xl` | ≥ 1536px |
519
+
520
+ ```ts
521
+ import { createResponsiveState, TailwindPreset, TailwindOrder } from 'responsive-media';
522
+
523
+ const state = createResponsiveState(TailwindPreset, { order: [...TailwindOrder] });
524
+ ```
525
+
526
+ ### `BootstrapPreset` + `BootstrapOrder`
527
+
528
+ Mutually exclusive Bootstrap 5 breakpoints:
529
+
530
+ | Key | Range |
531
+ |-------|----------------|
532
+ | `xs` | ≤ 575px |
533
+ | `sm` | 576 – 767px |
534
+ | `md` | 768 – 991px |
535
+ | `lg` | 992 – 1199px |
536
+ | `xl` | 1200 – 1399px |
537
+ | `xxl` | ≥ 1400px |
538
+
539
+ ```ts
540
+ import { createResponsiveState, BootstrapPreset, BootstrapOrder } from 'responsive-media';
541
+
542
+ const state = createResponsiveState(BootstrapPreset, { order: [...BootstrapOrder] });
543
+ ```
544
+
545
+ ### `AccessibilityPreset`
546
+
547
+ User-preference media queries. Multiple keys can be `true` simultaneously.
548
+
549
+ | Key | Matches when … |
550
+ |-----------------|-----------------------------------------|
551
+ | `dark` | `prefers-color-scheme: dark` |
552
+ | `light` | `prefers-color-scheme: light` |
553
+ | `reducedMotion` | `prefers-reduced-motion: reduce` |
554
+ | `highContrast` | `prefers-contrast: more` |
555
+ | `lowContrast` | `prefers-contrast: less` |
556
+ | `noHover` | `hover: none` (touch / stylus devices) |
557
+ | `coarsePointer` | `pointer: coarse` (finger-sized input) |
558
+ | `forcedColors` | `forced-colors: active` (Windows HCM) |
559
+ | `print` | `print` media type |
560
+
561
+ ```ts
562
+ import { createResponsiveState, AccessibilityPreset } from 'responsive-media';
563
+
564
+ const a11y = createResponsiveState(AccessibilityPreset);
565
+
566
+ a11y.onEnter('dark', () => applyDarkTheme());
567
+ a11y.onEnter('reducedMotion', () => disableAnimations());
568
+ a11y.onEnter('print', () => hideNonPrintable());
569
+ ```
570
+
571
+ ---
572
+
573
+ ## Vue 3 integration
574
+
575
+ Import from `responsive-media` (main entry) or `responsive-media` directly — Vue composables are included in the main bundle.
576
+
577
+ ### Plugin registration
578
+
579
+ ```ts
580
+ import { createApp } from 'vue';
581
+ import { ResponsivePlugin } from 'responsive-media';
582
+
583
+ const app = createApp(App);
584
+
585
+ app.use(ResponsivePlugin, {
586
+ sm: [{ type: 'max-width', value: 767 }],
587
+ lg: [{ type: 'min-width', value: 1024 }],
588
+ });
589
+
590
+ app.mount('#app');
591
+ ```
592
+
593
+ ### `useResponsive<T>()` — reactive state object
594
+
595
+ Returns the Vue reactive responsive state. Reactive in templates and computed properties.
596
+
597
+ ```vue
598
+ <script setup lang="ts">
599
+ import { useResponsive } from 'responsive-media';
600
+
601
+ type MyState = { sm: boolean; lg: boolean };
602
+ const state = useResponsive<MyState>();
603
+ </script>
604
+
605
+ <template>
606
+ <MobileNav v-if="state.sm" />
607
+ <DesktopNav v-else />
608
+ </template>
609
+ ```
610
+
611
+ ### `useBreakpoints()` — ordered helpers
612
+
613
+ Returns reactive ordered breakpoint helpers. All methods react to viewport changes in templates.
614
+
615
+ ```vue
616
+ <script setup>
617
+ import { useBreakpoints } from 'responsive-media';
618
+
619
+ const { current, isAbove, isBelow, between } = useBreakpoints();
620
+ </script>
621
+
622
+ <template>
623
+ <span>Current: {{ current }}</span>
624
+ <DesktopNav v-if="isAbove('sm')" />
625
+ <MobileNav v-else />
626
+ <TabletOnly v-if="between('sm', 'lg')" />
627
+ </template>
628
+ ```
629
+
630
+ > `current` is a `ComputedRef<string | null>`. `isAbove`, `isBelow`, `between` are plain functions — reactive because they read from the Vue reactive state.
631
+
632
+ ### `useMediaQuery(query)` — single raw query
633
+
634
+ Returns a `Ref<boolean>` for a raw CSS media query string. Cleans up automatically on `onUnmounted`.
635
+
636
+ ```vue
637
+ <script setup>
638
+ import { useMediaQuery } from 'responsive-media';
639
+
640
+ const isDark = useMediaQuery('(prefers-color-scheme: dark)');
641
+ const canHover = useMediaQuery('(hover: hover)');
642
+ </script>
643
+
644
+ <template>
645
+ <DarkTheme v-if="isDark" />
646
+ </template>
647
+ ```
648
+
649
+ ### `useContainerState(elementRef, config, options?)` — container queries
650
+
651
+ Tracks an element's dimensions and returns a reactive state object. Sets up and tears down the `ResizeObserver` automatically via `watchEffect`.
652
+
653
+ ```vue
654
+ <script setup>
655
+ import { useTemplateRef } from 'vue';
656
+ import { useContainerState } from 'responsive-media';
657
+
658
+ const cardRef = useTemplateRef('card');
659
+ const cardState = useContainerState(cardRef, {
660
+ compact: [{ type: 'max-width', value: 300 }],
661
+ wide: [{ type: 'min-width', value: 600 }],
662
+ });
663
+ </script>
664
+
665
+ <template>
666
+ <div ref="card">
667
+ <CompactLayout v-if="cardState.compact" />
668
+ <WideLayout v-else-if="cardState.wide" />
669
+ <DefaultLayout v-else />
670
+ </div>
671
+ </template>
672
+ ```
673
+
674
+ ---
675
+
676
+ ## React 18+ integration
677
+
678
+ Import from `responsive-media/react`.
679
+
680
+ ```ts
681
+ import { useResponsive, useBreakpoints, useMediaQuery, useContainerState } from 'responsive-media/react';
682
+ ```
683
+
684
+ ### `useResponsive<T>()` — reactive state
685
+
686
+ Returns the current responsive state. Re-renders only when state changes. Uses `useSyncExternalStore` internally.
687
+
688
+ ```tsx
689
+ import { useResponsive } from 'responsive-media/react';
690
+
691
+ type MyState = { sm: boolean; lg: boolean };
692
+
693
+ function App() {
694
+ const { sm, lg } = useResponsive<MyState>();
695
+ return sm ? <MobileNav /> : <DesktopNav />;
696
+ }
697
+ ```
698
+
699
+ ### `useBreakpoints()` — ordered helpers
700
+
701
+ Returns ordered breakpoint helpers. Re-renders when the responsive state changes.
702
+
703
+ ```tsx
704
+ import { useBreakpoints } from 'responsive-media/react';
705
+
706
+ function Nav() {
707
+ const { current, isAbove, isBelow, between } = useBreakpoints();
708
+ return (
709
+ <>
710
+ <span>Current: {current}</span>
711
+ {isAbove('sm') ? <DesktopNav /> : <MobileNav />}
712
+ {between('sm', 'lg') && <TabletBanner />}
713
+ </>
714
+ );
715
+ }
716
+ ```
717
+
718
+ > Unlike Vue, `current` is a plain `string | null` (not a ref). Re-renders are triggered by `useSyncExternalStore`.
719
+
720
+ ### `useMediaQuery(query)` — single raw query
721
+
722
+ Returns a `boolean` that tracks a raw CSS media query string. SSR-safe (returns `false` on the server).
723
+
724
+ ```tsx
725
+ import { useMediaQuery } from 'responsive-media/react';
726
+
727
+ function ThemeToggle() {
728
+ const isDark = useMediaQuery('(prefers-color-scheme: dark)');
729
+ const canHover = useMediaQuery('(hover: hover)');
730
+ return <button className={isDark ? 'dark' : 'light'}>Toggle</button>;
731
+ }
732
+ ```
733
+
734
+ ### `useContainerState(ref, config, options?)` — container queries
735
+
736
+ Tracks an element's dimensions and returns a state object. Sets up and tears down `ResizeObserver` via `useEffect`.
737
+
738
+ ```tsx
739
+ import { useRef } from 'react';
740
+ import { useContainerState } from 'responsive-media/react';
741
+
742
+ function Card() {
743
+ const ref = useRef<HTMLDivElement>(null);
744
+ const { compact, wide } = useContainerState(ref, {
745
+ compact: [{ type: 'max-width', value: 300 }],
746
+ wide: [{ type: 'min-width', value: 600 }],
747
+ });
748
+
749
+ return (
750
+ <div ref={ref}>
751
+ {compact ? <CompactLayout /> : wide ? <WideLayout /> : <DefaultLayout />}
752
+ </div>
753
+ );
754
+ }
755
+ ```
756
+
757
+ > `config` and `options` are treated as static after mount. Wrap in `useMemo` if they change.
758
+
759
+ ---
760
+
761
+ ## TypeScript helpers
762
+
763
+ ### `ConfigToState<T>`
764
+
765
+ Derives a boolean-state type from a config object:
766
+
767
+ ```ts
768
+ import type { ConfigToState, MediaQueryConfig } from 'responsive-media';
769
+
770
+ const config = {
771
+ sm: [{ type: 'min-width', value: 640 }],
772
+ lg: [{ type: 'min-width', value: 1024 }],
773
+ } satisfies Record<string, MediaQueryConfig>;
774
+
775
+ type MyState = ConfigToState<typeof config>;
776
+ // → { sm: boolean; lg: boolean }
777
+
778
+ const { sm, lg } = responsiveState.getState<MyState>();
779
+ ```
780
+
781
+ ### Generic `useResponsive<T>()`
782
+
783
+ Both Vue and React adapters accept a generic type parameter to narrow the returned state:
784
+
785
+ ```ts
786
+ type AppState = { mobile: boolean; tablet: boolean; desktop: boolean };
787
+ const state = useResponsive<AppState>();
788
+ ```
789
+
790
+ ---
791
+
792
+ ## SSR / hydration
793
+
794
+ All APIs are SSR-safe — they check for `window` and `matchMedia` availability before use and fall back to `false` on the server.
795
+
796
+ For hydration (preventing layout shift):
797
+
798
+ ```ts
799
+ // Server: serialize the expected initial state
800
+ const initialState = { mobile: false, tablet: false, desktop: true };
801
+
802
+ // Client: hydrate before the first render
803
+ import { responsiveState } from 'responsive-media';
804
+ responsiveState.hydrate(initialState);
805
+ ```
806
+
807
+ ---
808
+
809
+ ## Exported API reference
810
+
811
+ ### Main entry (`responsive-media`)
812
+
813
+ | Export | Type | Description |
814
+ |---------------------------|--------------------------|-------------------------------------------------------|
815
+ | `responsiveState` | `ReactiveResponsiveState`| Global singleton, default `ResponsiveConfig` |
816
+ | `setResponsiveConfig` | function | Reconfigure the global singleton |
817
+ | `getResponsiveState` | function | Get state snapshot from global singleton |
818
+ | `getResponsiveMediaQueries` | function | Get CSS query strings from global singleton |
819
+ | `createResponsiveState` | function | Create an isolated `ReactiveResponsiveState` instance |
820
+ | `createContainerState` | function | Create a `ContainerState` for an element |
821
+ | `toMediaQueryString` | function | Convert `MediaQueryConfig` to CSS string |
822
+ | `match` | function | Pick a value by first matching breakpoint key |
823
+ | `subscribeMediaQuery` | function | Subscribe to a raw CSS media query string |
824
+ | `ResponsiveConfig` | const | Default mobile/tablet/desktop breakpoints |
825
+ | `BaseResponsiveState` | class | Abstract base (for extension) |
826
+ | `ReactiveResponsiveState` | class | Viewport state (matchMedia-backed) |
827
+ | `ContainerState` | class | Element container state (ResizeObserver-backed) |
828
+ | `ResponsivePlugin` | Vue plugin | Vue app plugin |
829
+ | `useResponsive` | Vue composable | Reactive state object |
830
+ | `useBreakpoints` | Vue composable | Ordered breakpoint helpers |
831
+ | `useMediaQuery` | Vue composable | Single raw media query |
832
+ | `useContainerState` | Vue composable | Element container queries |
833
+ | `ConfigToState` | type | Derives state type from config |
834
+ | `MediaQueryConfig` | type | Config entry type |
835
+ | `MediaQueryCondition` | type | Single condition type |
836
+ | `ResponsiveState` | type | `Record<string, boolean>` |
837
+ | `SetConfigOptions` | type | Options for `setConfig` / `createResponsiveState` |
838
+ | `BreakpointHelpers` | type | Return type of `useBreakpoints` |
839
+
840
+ ### React entry (`responsive-media/react`)
841
+
842
+ | Export | Description |
843
+ |---------------------|----------------------------------------------|
844
+ | `useResponsive` | State hook (useSyncExternalStore) |
845
+ | `useBreakpoints` | Ordered breakpoint helpers hook |
846
+ | `useMediaQuery` | Single raw media query hook |
847
+ | `useContainerState` | Element container queries hook |
848
+ | `BreakpointHelpers` | Type for `useBreakpoints` return value |
849
+
850
+ ### Presets entry (`responsive-media/presets`)
851
+
852
+ | Export | Description |
853
+ |----------------------|-----------------------------------------|
854
+ | `TailwindPreset` | Tailwind CSS v3/v4 breakpoints |
855
+ | `TailwindOrder` | Ordered key array for `TailwindPreset` |
856
+ | `BootstrapPreset` | Bootstrap 5 breakpoints |
857
+ | `BootstrapOrder` | Ordered key array for `BootstrapPreset` |
858
+ | `AccessibilityPreset`| User-preference media queries |
859
+
860
+ ### Container entry (`responsive-media/container`)
861
+
862
+ | Export | Description |
863
+ |-----------------------|----------------------------------------------|
864
+ | `ContainerState` | Class for element container queries |
865
+ | `createContainerState`| Factory function |
866
+
867
+ ---
868
+
869
+ ## License
870
+
871
+ MIT
872
+
873
+ ## Author
874
+
875
+ Danil Lisin aka Macrulez
876
+
877
+ GitHub: [macrulezru](https://github.com/macrulezru) · Website: [macrulez.ru](https://macrulez.ru/)