@btja/use-tailwind-media-query 0.1.1 → 0.2.0

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,38 +1,30 @@
1
1
  # use-tailwind-media-query
2
2
 
3
- React hooks for Tailwind-style breakpoints and arbitrary media queries.
3
+ React 19 hooks for Tailwind-style breakpoints and arbitrary media queries.
4
4
 
5
- ## Motivation
6
-
7
- ### Adaptive design
8
-
9
-
10
- This hook is intended to be used in adaptive designs in React.
11
-
12
- Use adaptive design when layouts should change across breakpoints (for example: dashboards, dense data views, and flows on desktop vs mobile). Prefer plain responsive design when the same hierarchy still works and only spacing,
13
- columns, or typography need to change.
14
-
15
- ### Store
16
-
17
- The hooks share a small media-query store so repeated breakpoint checks can reuse the
18
- same underlying subscription instead of attaching duplicate `matchMedia` listeners in
19
- every component.
5
+ <table align="center">
6
+ <tr>
7
+ <td align="center">
8
+ <img src="docs/desktop-view.png" alt="Desktop adaptive view" height="320" />
9
+ <div><strong>Desktop view</strong></div>
10
+ </td>
11
+ <td align="center">
12
+ <img src="docs/mobile-view.png" alt="Mobile adaptive view" height="320" />
13
+ <div><strong>Mobile view</strong></div>
14
+ </td>
15
+ </tr>
16
+ </table>
20
17
 
21
- Multiple components on the same screen can listen to the
22
- same media query without duplicating listeners and wasting memory.
18
+ <div align="center">
19
+ <a href="https://bwt2.github.io/use-tailwind-media-query">Live demo</a>
20
+ </div>
23
21
 
24
22
  ## Installation
25
23
 
26
24
  ```bash
27
25
  npm i @btja/use-tailwind-media-query
28
- ```
29
- ```bash
30
26
  yarn add @btja/use-tailwind-media-query
31
- ```
32
- ```bash
33
27
  pnpm add @btja/use-tailwind-media-query
34
- ```
35
- ```bash
36
28
  bun add @btja/use-tailwind-media-query
37
29
  ```
38
30
 
@@ -64,8 +56,8 @@ Hooks:
64
56
 
65
57
  | Hook | Returns | Purpose |
66
58
  | --- | --- | --- |
67
- | `useMediaQuery(query: string)` | `bool` | Subscribe to any CSS media query |
68
- | `useTailwindBreakpoint(tailwindBreakpoint: TailwindBreakpoint)` | `bool` | Tailwind breakpoint helper |
59
+ | `useMediaQuery(query: string, options?)` | `bool` | Subscribe to any CSS media query |
60
+ | `useTailwindBreakpoint(tailwindBreakpoint: TailwindBreakpoint, options?)` | `bool` | Tailwind breakpoint helper |
69
61
  | `useIsMobile()` | `bool` | `true` below `md` |
70
62
  | `useIsTablet()` | `bool` | `true` from `md` to below `lg` |
71
63
  | `useIsDesktop()` | `bool` | `true` at `lg` and above |
@@ -85,19 +77,6 @@ export function Example() {
85
77
  }
86
78
  ```
87
79
 
88
- <table align="center">
89
- <tr>
90
- <td align="center">
91
- <img src="docs/desktop-view.png" alt="Desktop adaptive view" height="320" />
92
- <div><strong>Desktop view</strong></div>
93
- </td>
94
- <td align="center">
95
- <img src="docs/mobile-view.png" alt="Mobile adaptive view" height="320" />
96
- <div><strong>Mobile view</strong></div>
97
- </td>
98
- </tr>
99
- </table>
100
-
101
80
  `useTailwindBreakpoint`:
102
81
 
103
82
  ```tsx
@@ -110,6 +89,16 @@ export function SidebarLayout() {
110
89
  }
111
90
  ```
112
91
 
92
+ ```tsx
93
+ import { useTailwindBreakpoint } from '@btja/use-tailwind-media-query'
94
+
95
+ export function SsrSidebarLayout() {
96
+ const showSidebar = useTailwindBreakpoint('lg', { noWindowDefault: true })
97
+
98
+ return showSidebar ? <DesktopLayout /> : <CompactLayout />
99
+ }
100
+ ```
101
+
113
102
  `useMediaQuery`:
114
103
 
115
104
  ```tsx
@@ -122,18 +111,31 @@ export function MotionPreference() {
122
111
  }
123
112
  ```
124
113
 
114
+ ```tsx
115
+ import { useMediaQuery } from '@btja/use-tailwind-media-query'
116
+
117
+ export function SsrDesktopShell() {
118
+ const isDesktop = useMediaQuery('(min-width: 1024px)', { noWindowDefault: true })
119
+
120
+ return isDesktop ? <DesktopShell /> : <MobileShell />
121
+ }
122
+ ```
123
+
125
124
 
126
125
  ## SEO and SSR
127
126
 
128
127
  If possible, keep both screen variants semantically equivalent for SEO, accessibility,
129
128
  and consistency purposes.
130
129
 
131
- With this library, server render does not know the real viewport:
130
+ With this library, server render does not know the real viewport unless you provide an
131
+ explicit fallback with `noWindowDefault`:
132
132
 
133
133
  | Hook | Server-render value |
134
134
  | --- | --- |
135
135
  | `useMediaQuery()` | `false` |
136
+ | `useMediaQuery(query, { noWindowDefault: true })` | `true` |
136
137
  | `useTailwindBreakpoint()` | `false` |
138
+ | `useTailwindBreakpoint(breakpoint, { noWindowDefault: true })` | `true` |
137
139
  | `useIsMobile()` | `true` |
138
140
  | `useIsDesktop()` | `false` |
139
141
 
@@ -145,6 +147,19 @@ return useIsMobile() ? <MobileView /> : <DesktopView />
145
147
 
146
148
  renders the mobile branch on the server, then hydrates to the real client viewport.
147
149
 
150
+ Use `noWindowDefault` when you want the server-rendered branch to default to a known
151
+ assumption before the browser can evaluate `matchMedia`. This is useful when your SSR
152
+ environment already has a strong hint about the likely viewport and you want to reduce
153
+ layout flipping during hydration.
154
+
155
+ Ways to choose `noWindowDefault` on the server include:
156
+ - infer a mobile/desktop default from the user-agent string
157
+ - use device hints or request headers provided by your framework, CDN, or edge runtime
158
+ - store a prior device preference in a cookie and reuse it during SSR
159
+ - pick a product-level default such as mobile-first when no reliable hint exists
160
+
161
+ If the server guess is wrong, the client will correct to the real media-query result after hydration (with risk of layout flash).
162
+
148
163
  If SEO matters, avoid putting unique primary content in only one branch. Keep headings,
149
164
  copy, links, and other canonical content the same when possible, and reserve screen-
150
165
  specific differences for layout or secondary UI.
@@ -154,6 +169,26 @@ Current limitation: `useMediaQuery` depends on `MediaQueryList.addEventListener`
154
169
  `removeEventListener`. Older Safari/WebView environments that only implement
155
170
  `addListener` / `removeListener` are not supported.
156
171
 
172
+
173
+ ## Motivation
174
+
175
+ ### Adaptive design
176
+
177
+
178
+ This hook is intended to be used in adaptive designs in React.
179
+
180
+ Use adaptive design when layouts should change across breakpoints (for example: dashboards, dense data views, and flows on desktop vs mobile). Prefer plain responsive design when the same hierarchy still works and only spacing,
181
+ columns, or typography need to change.
182
+
183
+ ### Store
184
+
185
+ The hooks share a small media-query store so repeated breakpoint checks can reuse the
186
+ same underlying subscription instead of attaching duplicate `matchMedia` listeners in
187
+ every component.
188
+
189
+ Multiple components on the same screen can listen to the
190
+ same media query without duplicating listeners and wasting memory.
191
+
157
192
  ## Development
158
193
  Using pnpm.
159
194
 
@@ -174,14 +209,14 @@ pnpm run build
174
209
  Before releasing (after `npm login`):
175
210
 
176
211
  ```bash
177
- pnpm bump:version
178
212
  pnpm check:release
213
+ pnpm bump:version
214
+
179
215
  npm publish --access public
180
216
  ```
181
217
 
182
218
  ## AI Disclosure
183
- Codex 5.4 was used to review and sanity-check:
184
- - tsdown configs using the official [tsdown agent skill](https://tsdown.dev/guide/skills).
185
- - React hook implementation for any issues.
186
-
187
- Codex 5.4 was used to generate mockup UI in '`./playground`' demo.
219
+ Codex 5.4 was used to review and sanity-check the code. Codex was also used for:
220
+ - Mockup UI in '`./playground`' demo
221
+ - JSDoc
222
+ - Tests
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react/compiler-runtime"),t=require("react");const n=new Map;function r(e){let t=typeof window<`u`?window.matchMedia(e):void 0,r=new Set,i=()=>{r.forEach(e=>e())};return{getSnapshot(){return(t==null?void 0:t.matches)??!1},getServerSnapshot(){return!1},subscribe(a){return t?(r.add(a),r.size===1&&t.addEventListener(`change`,i),()=>{r.delete(a),r.size===0&&(t.removeEventListener(`change`,i),n.delete(e))}):()=>{}}}}function i(e){let t=n.get(e);return t||(t=r(e),n.set(e,t)),t}function a(n){let r=(0,e.c)(2),a;r[0]===n?a=r[1]:(a=i(n),r[0]=n,r[1]=a);let o=a;return(0,t.useSyncExternalStore)(o.subscribe,o.getSnapshot,o.getServerSnapshot)}const o={sm:`(min-width: 640px)`,md:`(min-width: 768px)`,lg:`(min-width: 1024px)`,xl:`(min-width: 1280px)`,"2xl":`(min-width: 1536px)`};function s(e){return a(o[e])}function c(){return!s(`md`)}function l(){let e=s(`md`),t=s(`lg`);return e&&!t}function u(){return s(`lg`)}function d(){return s(`xl`)}exports.useIsDesktop=u,exports.useIsLargeDesktop=d,exports.useIsMobile=c,exports.useIsTablet=l,exports.useMediaQuery=a,exports.useTailwindBreakpoint=s;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("react/compiler-runtime"),t=require("react");const n=new Map;function r(e,t=!1,r=e){let i=typeof window<`u`?window.matchMedia(e):void 0,a=new Set,o=()=>{a.forEach(e=>e())};return{getSnapshot(){return(i==null?void 0:i.matches)??t},getServerSnapshot(){return t},subscribe(e){return i?(a.add(e),a.size===1&&i.addEventListener(`change`,o),()=>{a.delete(e),a.size===0&&(i.removeEventListener(`change`,o),n.delete(r))}):()=>{}}}}function i(e,t=!1){if(typeof window>`u`)return r(e,t);let i=`${e}::${String(t)}`,a=n.get(i);return a||(a=r(e,t,i),n.set(i,a)),a}function a(n,r){let a=(0,e.c)(5),o;a[0]===r?o=a[1]:(o=r===void 0?{}:r,a[0]=r,a[1]=o);let s=o.noWindowDefault??!1,c;a[2]!==n||a[3]!==s?(c=i(n,s),a[2]=n,a[3]=s,a[4]=c):c=a[4];let l=c;return(0,t.useSyncExternalStore)(l.subscribe,l.getSnapshot,l.getServerSnapshot)}const o={sm:`(min-width: 640px)`,md:`(min-width: 768px)`,lg:`(min-width: 1024px)`,xl:`(min-width: 1280px)`,"2xl":`(min-width: 1536px)`};function s(e,t){return a(o[e],t)}function c(){return!s(`md`)}function l(){let e=s(`md`),t=s(`lg`);return e&&!t}function u(){return s(`lg`)}function d(){return s(`xl`)}exports.useIsDesktop=u,exports.useIsLargeDesktop=d,exports.useIsMobile=c,exports.useIsTablet=l,exports.useMediaQuery=a,exports.useTailwindBreakpoint=s;
package/dist/index.d.cts CHANGED
@@ -1,7 +1,19 @@
1
1
  //#region src/useMediaQuery.d.ts
2
- declare function useMediaQuery(query: string): boolean;
2
+ type UseMediaQueryOptions = {
3
+ /**
4
+ * Value returned when `window` is unavailable, such as during server rendering.
5
+ */
6
+ noWindowDefault?: boolean;
7
+ };
8
+ /**
9
+ * Subscribes to an arbitrary CSS media query and returns whether it currently matches.
10
+ */
11
+ declare function useMediaQuery(query: string, options?: UseMediaQueryOptions): boolean;
3
12
  //#endregion
4
13
  //#region src/tailwindBreakpoints.d.ts
14
+ /**
15
+ * Built-in Tailwind-style media queries keyed by breakpoint name.
16
+ */
5
17
  declare const tailwindBreakpointQueries: {
6
18
  readonly sm: "(min-width: 640px)";
7
19
  readonly md: "(min-width: 768px)";
@@ -9,13 +21,31 @@ declare const tailwindBreakpointQueries: {
9
21
  readonly xl: "(min-width: 1280px)";
10
22
  readonly "2xl": "(min-width: 1536px)";
11
23
  };
24
+ /**
25
+ * Supported Tailwind breakpoint names for the built-in breakpoint helpers.
26
+ */
12
27
  type TailwindBreakpoint = keyof typeof tailwindBreakpointQueries;
13
28
  //#endregion
14
29
  //#region src/useTailwindBreakpoint.d.ts
15
- declare function useTailwindBreakpoint(tailwindBreakpoint: TailwindBreakpoint): boolean;
30
+ /**
31
+ * Subscribes to one of the built-in Tailwind breakpoint queries.
32
+ */
33
+ declare function useTailwindBreakpoint(tailwindBreakpoint: TailwindBreakpoint, options?: UseMediaQueryOptions): boolean;
34
+ /**
35
+ * Returns `true` when the viewport is smaller than Tailwind's `md` breakpoint.
36
+ */
16
37
  declare function useIsMobile(): boolean;
38
+ /**
39
+ * Returns `true` when the viewport is between Tailwind's `md` and `lg` breakpoints.
40
+ */
17
41
  declare function useIsTablet(): boolean;
42
+ /**
43
+ * Returns `true` when the viewport is at or above Tailwind's `lg` breakpoint.
44
+ */
18
45
  declare function useIsDesktop(): boolean;
46
+ /**
47
+ * Returns `true` when the viewport is at or above Tailwind's `xl` breakpoint.
48
+ */
19
49
  declare function useIsLargeDesktop(): boolean;
20
50
  //#endregion
21
- export { type TailwindBreakpoint, type tailwindBreakpointQueries, useIsDesktop, useIsLargeDesktop, useIsMobile, useIsTablet, useMediaQuery, useTailwindBreakpoint };
51
+ export { type TailwindBreakpoint, UseMediaQueryOptions, type tailwindBreakpointQueries, useIsDesktop, useIsLargeDesktop, useIsMobile, useIsTablet, useMediaQuery, useTailwindBreakpoint };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,19 @@
1
1
  //#region src/useMediaQuery.d.ts
2
- declare function useMediaQuery(query: string): boolean;
2
+ type UseMediaQueryOptions = {
3
+ /**
4
+ * Value returned when `window` is unavailable, such as during server rendering.
5
+ */
6
+ noWindowDefault?: boolean;
7
+ };
8
+ /**
9
+ * Subscribes to an arbitrary CSS media query and returns whether it currently matches.
10
+ */
11
+ declare function useMediaQuery(query: string, options?: UseMediaQueryOptions): boolean;
3
12
  //#endregion
4
13
  //#region src/tailwindBreakpoints.d.ts
14
+ /**
15
+ * Built-in Tailwind-style media queries keyed by breakpoint name.
16
+ */
5
17
  declare const tailwindBreakpointQueries: {
6
18
  readonly sm: "(min-width: 640px)";
7
19
  readonly md: "(min-width: 768px)";
@@ -9,13 +21,31 @@ declare const tailwindBreakpointQueries: {
9
21
  readonly xl: "(min-width: 1280px)";
10
22
  readonly "2xl": "(min-width: 1536px)";
11
23
  };
24
+ /**
25
+ * Supported Tailwind breakpoint names for the built-in breakpoint helpers.
26
+ */
12
27
  type TailwindBreakpoint = keyof typeof tailwindBreakpointQueries;
13
28
  //#endregion
14
29
  //#region src/useTailwindBreakpoint.d.ts
15
- declare function useTailwindBreakpoint(tailwindBreakpoint: TailwindBreakpoint): boolean;
30
+ /**
31
+ * Subscribes to one of the built-in Tailwind breakpoint queries.
32
+ */
33
+ declare function useTailwindBreakpoint(tailwindBreakpoint: TailwindBreakpoint, options?: UseMediaQueryOptions): boolean;
34
+ /**
35
+ * Returns `true` when the viewport is smaller than Tailwind's `md` breakpoint.
36
+ */
16
37
  declare function useIsMobile(): boolean;
38
+ /**
39
+ * Returns `true` when the viewport is between Tailwind's `md` and `lg` breakpoints.
40
+ */
17
41
  declare function useIsTablet(): boolean;
42
+ /**
43
+ * Returns `true` when the viewport is at or above Tailwind's `lg` breakpoint.
44
+ */
18
45
  declare function useIsDesktop(): boolean;
46
+ /**
47
+ * Returns `true` when the viewport is at or above Tailwind's `xl` breakpoint.
48
+ */
19
49
  declare function useIsLargeDesktop(): boolean;
20
50
  //#endregion
21
- export { type TailwindBreakpoint, type tailwindBreakpointQueries, useIsDesktop, useIsLargeDesktop, useIsMobile, useIsTablet, useMediaQuery, useTailwindBreakpoint };
51
+ export { type TailwindBreakpoint, UseMediaQueryOptions, type tailwindBreakpointQueries, useIsDesktop, useIsLargeDesktop, useIsMobile, useIsTablet, useMediaQuery, useTailwindBreakpoint };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{c as e}from"react/compiler-runtime";import{useSyncExternalStore as t}from"react";const n=new Map;function r(e){let t=typeof window<`u`?window.matchMedia(e):void 0,r=new Set,i=()=>{r.forEach(e=>e())};return{getSnapshot(){return(t==null?void 0:t.matches)??!1},getServerSnapshot(){return!1},subscribe(a){return t?(r.add(a),r.size===1&&t.addEventListener(`change`,i),()=>{r.delete(a),r.size===0&&(t.removeEventListener(`change`,i),n.delete(e))}):()=>{}}}}function i(e){let t=n.get(e);return t||(t=r(e),n.set(e,t)),t}function a(n){let r=e(2),a;r[0]===n?a=r[1]:(a=i(n),r[0]=n,r[1]=a);let o=a;return t(o.subscribe,o.getSnapshot,o.getServerSnapshot)}const o={sm:`(min-width: 640px)`,md:`(min-width: 768px)`,lg:`(min-width: 1024px)`,xl:`(min-width: 1280px)`,"2xl":`(min-width: 1536px)`};function s(e){return a(o[e])}function c(){return!s(`md`)}function l(){let e=s(`md`),t=s(`lg`);return e&&!t}function u(){return s(`lg`)}function d(){return s(`xl`)}export{u as useIsDesktop,d as useIsLargeDesktop,c as useIsMobile,l as useIsTablet,a as useMediaQuery,s as useTailwindBreakpoint};
1
+ import{c as e}from"react/compiler-runtime";import{useSyncExternalStore as t}from"react";const n=new Map;function r(e,t=!1,r=e){let i=typeof window<`u`?window.matchMedia(e):void 0,a=new Set,o=()=>{a.forEach(e=>e())};return{getSnapshot(){return(i==null?void 0:i.matches)??t},getServerSnapshot(){return t},subscribe(e){return i?(a.add(e),a.size===1&&i.addEventListener(`change`,o),()=>{a.delete(e),a.size===0&&(i.removeEventListener(`change`,o),n.delete(r))}):()=>{}}}}function i(e,t=!1){if(typeof window>`u`)return r(e,t);let i=`${e}::${String(t)}`,a=n.get(i);return a||(a=r(e,t,i),n.set(i,a)),a}function a(n,r){let a=e(5),o;a[0]===r?o=a[1]:(o=r===void 0?{}:r,a[0]=r,a[1]=o);let s=o.noWindowDefault??!1,c;a[2]!==n||a[3]!==s?(c=i(n,s),a[2]=n,a[3]=s,a[4]=c):c=a[4];let l=c;return t(l.subscribe,l.getSnapshot,l.getServerSnapshot)}const o={sm:`(min-width: 640px)`,md:`(min-width: 768px)`,lg:`(min-width: 1024px)`,xl:`(min-width: 1280px)`,"2xl":`(min-width: 1536px)`};function s(e,t){return a(o[e],t)}function c(){return!s(`md`)}function l(){let e=s(`md`),t=s(`lg`);return e&&!t}function u(){return s(`lg`)}function d(){return s(`xl`)}export{u as useIsDesktop,d as useIsLargeDesktop,c as useIsMobile,l as useIsTablet,a as useMediaQuery,s as useTailwindBreakpoint};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@btja/use-tailwind-media-query",
3
3
  "type": "module",
4
- "version": "0.1.1",
4
+ "version": "0.2.0",
5
5
  "description": "Helper react hook to listen for tailwind media breakpoints. Intended for use with adaptive design.",
6
6
  "author": "Brian W Tjahjadi <brian.w.tjahjadi@gmail.com>",
7
7
  "license": "MIT",
@@ -13,7 +13,10 @@
13
13
  "react-hooks",
14
14
  "hook",
15
15
  "custom-hook",
16
- "use-tailwind-media-query"
16
+ "use-tailwind-media-query",
17
+ "tailwind-media-query",
18
+ "tailwind-breakpoints",
19
+ "adaptive-design"
17
20
  ],
18
21
  "homepage": "https://github.com/bwt2/use-tailwind-media-query#readme",
19
22
  "repository": {
@@ -37,6 +40,7 @@
37
40
  "build": "tsdown",
38
41
  "dev": "tsdown --watch",
39
42
  "play": "vite",
43
+ "play:build": "vite build",
40
44
  "test": "vitest",
41
45
  "typecheck": "tsc --noEmit",
42
46
  "pack:dry-run": "npm pack --dry-run",