@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 +21 -0
- package/README.md +175 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +1 -0
- package/package.json +69 -0
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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|