@btja/use-tailwind-media-query 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brian T
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # use-tailwind-media-query
2
+
3
+ React hooks for Tailwind-style breakpoints and arbitrary media queries.
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.
20
+
21
+ Multiple components on the same screen can listen to the
22
+ same media query without duplicating listeners and wasting memory.
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm i @btja/use-tailwind-media-query
28
+ ```
29
+ ```bash
30
+ yarn add @btja/use-tailwind-media-query
31
+ ```
32
+ ```bash
33
+ pnpm add @btja/use-tailwind-media-query
34
+ ```
35
+ ```bash
36
+ bun add @btja/use-tailwind-media-query
37
+ ```
38
+
39
+ ## API
40
+
41
+ ```tsx
42
+ import {
43
+ useTailwindBreakpoint,
44
+ useIsDesktop,
45
+ useIsLargeDesktop,
46
+ useIsMobile,
47
+ useIsTablet,
48
+ useMediaQuery,
49
+ } from '@btja/use-tailwind-media-query'
50
+ import type { TailwindBreakpoint } from '@btja/use-tailwind-media-query'
51
+ ```
52
+
53
+ Built-in tailwind breakpoints (`TailwindBreakpoint`):
54
+
55
+ | Breakpoint | Min width |
56
+ | --- | --- |
57
+ | `sm` | `640px` |
58
+ | `md` | `768px` |
59
+ | `lg` | `1024px` |
60
+ | `xl` | `1280px` |
61
+ | `2xl` | `1536px` |
62
+
63
+ Hooks:
64
+
65
+ | Hook | Returns | Purpose |
66
+ | --- | --- | --- |
67
+ | `useMediaQuery(query: string)` | `bool` | Subscribe to any CSS media query |
68
+ | `useTailwindBreakpoint(tailwindBreakpoint: TailwindBreakpoint)` | `bool` | Tailwind breakpoint helper |
69
+ | `useIsMobile()` | `bool` | `true` below `md` |
70
+ | `useIsTablet()` | `bool` | `true` from `md` to below `lg` |
71
+ | `useIsDesktop()` | `bool` | `true` at `lg` and above |
72
+ | `useIsLargeDesktop()` | `bool` | `true` at `xl` and above |
73
+
74
+ ## Examples
75
+
76
+ `useIsMobile` (`useIsTablet`, `useIsDesktop`, `useIsLargeDesktop`):
77
+
78
+ ```tsx
79
+ import { useIsMobile } from '@btja/use-tailwind-media-query'
80
+
81
+ export function Example() {
82
+ const isMobile = useIsMobile()
83
+
84
+ return isMobile ? <MobileView /> : <DesktopView />
85
+ }
86
+ ```
87
+
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
+ `useTailwindBreakpoint`:
102
+
103
+ ```tsx
104
+ import { useTailwindBreakpoint } from '@btja/use-tailwind-media-query'
105
+
106
+ export function SidebarLayout() {
107
+ const showSidebar = useTailwindBreakpoint('lg')
108
+
109
+ return showSidebar ? <DesktopLayout /> : <CompactLayout />
110
+ }
111
+ ```
112
+
113
+ `useMediaQuery`:
114
+
115
+ ```tsx
116
+ import { useMediaQuery } from '@btja/use-tailwind-media-query'
117
+
118
+ export function MotionPreference() {
119
+ const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')
120
+
121
+ return <p>{prefersReducedMotion ? 'Reduced motion' : 'Full motion'}</p>
122
+ }
123
+ ```
124
+
125
+
126
+ ## SEO and SSR
127
+
128
+ If possible, keep both screen variants semantically equivalent. Adaptive design should
129
+ usually change layout and density, not core content. That is safer for SEO, accessibility,
130
+ and consistency.
131
+
132
+ With this library, server render does not know the real viewport:
133
+
134
+ | Hook | Server-render value |
135
+ | --- | --- |
136
+ | `useMediaQuery()` | `false` |
137
+ | `useTailwindBreakpoint()` | `false` |
138
+ | `useIsMobile()` | `true` |
139
+ | `useIsDesktop()` | `false` |
140
+
141
+ So this:
142
+
143
+ ```tsx
144
+ return useIsMobile() ? <MobileView /> : <DesktopView />
145
+ ```
146
+
147
+ renders the mobile branch on the server, then hydrates to the real client viewport.
148
+
149
+ If SEO matters, avoid putting unique primary content in only one branch. Keep headings,
150
+ copy, links, and other canonical content the same when possible, and reserve screen-
151
+ specific differences for layout or secondary UI.
152
+
153
+ ## Limitations
154
+ Current limitation: `useMediaQuery` depends on `MediaQueryList.addEventListener` /
155
+ `removeEventListener`. Older Safari/WebView environments that only implement
156
+ `addListener` / `removeListener` are not supported.
157
+
158
+ ## Development
159
+ Using pnpm.
160
+
161
+ ```bash
162
+ pnpm install
163
+ pnpm run play # run vite playground
164
+ pnpm run test
165
+ pnpm run typecheck
166
+ pnpm run build
167
+ ```
168
+
169
+ Before releasing (after `npm login`):
170
+
171
+ ```bash
172
+ pnpm bump:version
173
+ pnpm check:release
174
+ npm publish --access public
175
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +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;
@@ -0,0 +1,21 @@
1
+ //#region src/useMediaQuery.d.ts
2
+ declare function useMediaQuery(query: string): boolean;
3
+ //#endregion
4
+ //#region src/tailwindBreakpoints.d.ts
5
+ declare const tailwindBreakpointQueries: {
6
+ readonly sm: "(min-width: 640px)";
7
+ readonly md: "(min-width: 768px)";
8
+ readonly lg: "(min-width: 1024px)";
9
+ readonly xl: "(min-width: 1280px)";
10
+ readonly "2xl": "(min-width: 1536px)";
11
+ };
12
+ type TailwindBreakpoint = keyof typeof tailwindBreakpointQueries;
13
+ //#endregion
14
+ //#region src/useTailwindBreakpoint.d.ts
15
+ declare function useTailwindBreakpoint(tailwindBreakpoint: TailwindBreakpoint): boolean;
16
+ declare function useIsMobile(): boolean;
17
+ declare function useIsTablet(): boolean;
18
+ declare function useIsDesktop(): boolean;
19
+ declare function useIsLargeDesktop(): boolean;
20
+ //#endregion
21
+ export { type TailwindBreakpoint, type tailwindBreakpointQueries, useIsDesktop, useIsLargeDesktop, useIsMobile, useIsTablet, useMediaQuery, useTailwindBreakpoint };
@@ -0,0 +1,21 @@
1
+ //#region src/useMediaQuery.d.ts
2
+ declare function useMediaQuery(query: string): boolean;
3
+ //#endregion
4
+ //#region src/tailwindBreakpoints.d.ts
5
+ declare const tailwindBreakpointQueries: {
6
+ readonly sm: "(min-width: 640px)";
7
+ readonly md: "(min-width: 768px)";
8
+ readonly lg: "(min-width: 1024px)";
9
+ readonly xl: "(min-width: 1280px)";
10
+ readonly "2xl": "(min-width: 1536px)";
11
+ };
12
+ type TailwindBreakpoint = keyof typeof tailwindBreakpointQueries;
13
+ //#endregion
14
+ //#region src/useTailwindBreakpoint.d.ts
15
+ declare function useTailwindBreakpoint(tailwindBreakpoint: TailwindBreakpoint): boolean;
16
+ declare function useIsMobile(): boolean;
17
+ declare function useIsTablet(): boolean;
18
+ declare function useIsDesktop(): boolean;
19
+ declare function useIsLargeDesktop(): boolean;
20
+ //#endregion
21
+ export { type TailwindBreakpoint, type tailwindBreakpointQueries, useIsDesktop, useIsLargeDesktop, useIsMobile, useIsTablet, useMediaQuery, useTailwindBreakpoint };
package/dist/index.js ADDED
@@ -0,0 +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};
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@btja/use-tailwind-media-query",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "Helper react hook to listen for tailwind media breakpoints. Intended for use with adaptive design.",
6
+ "author": "Brian W Tjahjadi <brian.w.tjahjadi@gmail.com>",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/bwt2/use-tailwind-media-query#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/bwt2/use-tailwind-media-query.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/bwt2/use-tailwind-media-query/issues"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "sideEffects": false,
23
+ "engines": {
24
+ "node": ">=14.18.0"
25
+ },
26
+ "scripts": {
27
+ "build": "tsdown",
28
+ "dev": "tsdown --watch",
29
+ "play": "vite",
30
+ "test": "vitest",
31
+ "typecheck": "tsc --noEmit",
32
+ "pack:dry-run": "npm pack --dry-run",
33
+ "check:release": "pnpm run build && pnpm run test -- --run && pnpm run typecheck && pnpm run pack:dry-run",
34
+ "bump:version": "bumpp",
35
+ "prepublishOnly": "pnpm run build"
36
+ },
37
+ "peerDependencies": {
38
+ "react": "^19.2.0",
39
+ "react-dom": "^19.2.0"
40
+ },
41
+ "devDependencies": {
42
+ "@arethetypeswrong/core": "^0.18.4",
43
+ "@rolldown/plugin-babel": "^0.2.3",
44
+ "@tsconfig/strictest": "^2.0.8",
45
+ "@types/node": "^25.6.2",
46
+ "@types/react": "^19.2.14",
47
+ "@types/react-dom": "^19.2.3",
48
+ "@vitejs/plugin-react": "^6.0.1",
49
+ "@vitest/browser-playwright": "^4.1.5",
50
+ "babel-plugin-react-compiler": "^1.0.0",
51
+ "bumpp": "^11.1.0",
52
+ "publint": "^0.3.21",
53
+ "tsdown": "^0.22.0",
54
+ "typescript": "^6.0.3",
55
+ "vite": "^8.0.11",
56
+ "vitest": "^4.1.5",
57
+ "vitest-browser-react": "^2.2.0"
58
+ },
59
+ "main": "./dist/index.cjs",
60
+ "module": "./dist/index.js",
61
+ "types": "./dist/index.d.cts",
62
+ "exports": {
63
+ ".": {
64
+ "import": "./dist/index.js",
65
+ "require": "./dist/index.cjs"
66
+ },
67
+ "./package.json": "./package.json"
68
+ }
69
+ }