├── .gitignore
├── .npmignore
├── README.md
├── dist
├── helpers.d.ts
├── helpers.js
├── index.d.ts
├── index.js
├── plugin.d.ts
└── plugin.js
├── package.json
├── playground
├── README.md
├── markup.html
└── styles.css
├── src
├── helpers.ts
├── index.ts
└── plugin.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | .DS_store
3 | experiment.ts
4 | .turbo
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /src
2 | /playground
3 | tsconfig.json
4 | .vscode
5 | experiment.ts
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Tailwind Extended Shadows
2 |
3 | A TailwindCSS plugin that gives you fine-grain control over your box-shadows via simple utility classes (including magic utilities for auto-generating beautifully layered/stacked shadows).
4 |
5 | **Visual Demo Playground:** https://play.tailwindcss.com/6rFqo93e6h
6 |
7 | ## Table of Contents
8 |
9 | - [Usage](#usage)
10 | - [1. Shadow x & y offsets](#1-control-box-shadow-x--y-offsets)
11 | - [2. Shadow blur](#2-control-box-shadow-blur)
12 | - [3. Shadow spread](#3-control-box-shadow-spread)
13 | - [4. Shadow opacity](#4-control-box-shadow-opacity)
14 | - [5. Shadow layering/stacking](#shadow-layeringstacking)
15 | - [Control the number of layers](#1-control-the-number-of-layers)
16 | - [Control how the layers "scale"](#2-control-how-the-layers-scale)
17 | - [Control easing of layer scaling](#3-apply-easing-function-to-adjust-how-layers-scale)
18 | - [Layering Tips](#layering-tips)
19 | - [CSS Output](#css-output)
20 | - [Installation](#install)
21 | - [tailwind-merge compatibility plugin](#tailwind-merge-compatibility-plugin)
22 |
23 | ## Usage
24 |
25 | ### 1. Control box-shadow **`x` & `y` offsets**
26 |
27 | **Class Syntax**: `shadow-{x|y}-{theme.boxShadowOffset}`
28 |
29 | **Description**: Shifts the shadow's position in the direction you specify
30 |
31 | **Examples**:
32 |
33 | - `shadow-y-1` (pulls shadow downwards by `theme.boxShadowOffset.1` units),
34 | - `-shadow-y-2` (pulls shadow upwards by `theme.boxShadowOffset.2` units),
35 | - `shadow-x-px` (pulls shadow to the right by `1px`),
36 | - `-shadow-x-[3px]` (pulls shadow to the left by `3px` via arbitrary values syntax)
37 |
38 | **Configure**: Override/extend offset classes/values via `tailwind.config.js` > `theme` > `extend` > `boxShadowOffset`. Learn more about Tailwind theming [here](https://tailwindcss.com/docs/theme#extending-the-default-theme).
39 |
40 |
41 | Default Theme Values:
42 | `theme.boxShadowOffset` defaults to:
43 |
44 | ```js
45 | module.exports = {
46 | /* ... */
47 | theme: {
48 | boxShadowOffset: {
49 | px: "1px",
50 | 0: "0",
51 | 0.5: "0.125rem",
52 | 1: "0.25rem",
53 | 1.5: "0.375rem",
54 | 2: "0.5rem",
55 | 2.5: "0.625rem",
56 | 3: "0.75rem",
57 | 3.5: "0.875rem",
58 | 4: "1rem",
59 | 5: "1.25rem",
60 | 6: "1.5rem",
61 | 7: "1.75rem",
62 | 8: "2rem",
63 | },
64 | },
65 | };
66 | ```
67 |
68 |
69 |
70 | ---
71 |
72 | ### 2. Control box-shadow **`blur`**
73 |
74 | **Class Syntax**: `shadow-blur-{theme.boxShadowBlur}`
75 |
76 | **Description**: Controls the sharpness/softness of the shadow.
77 |
78 | **Examples**:
79 |
80 | - `shadow-blur-1` (blurs the shadow by `theme.boxShadowBlur.1` units),
81 | - `shadow-blur-2` (blurs the shadow by `theme.boxShadowBlur.2` units),
82 | - `shadow-blur-px` (blurs the shadow by `1px`),
83 | - `shadow-blur-[3px]` (blurs the shadow by `3px` via arbitrary values syntax)
84 |
85 | **Configure**: Override/extend blur classes/values via `tailwind.config.js` > `theme` > `extend` > `boxShadowBlur`.
86 |
87 |
88 | Default Theme Values:
89 | `theme.boxShadowBlur` defaults to:
90 |
91 | ```js
92 | module.exports = {
93 | /* ... */
94 | theme: {
95 | boxShadowBlur: {
96 | px: "1px",
97 | 0: "0",
98 | 0.5: "0.125rem",
99 | 1: "0.25rem",
100 | 1.5: "0.375rem",
101 | 2: "0.5rem",
102 | 2.5: "0.625rem",
103 | 3: "0.75rem",
104 | 3.5: "0.875rem",
105 | 4: "1rem",
106 | 5: "1.25rem",
107 | 6: "1.5rem",
108 | 7: "1.75rem",
109 | 8: "2rem",
110 | 9: "2.25rem",
111 | 10: "2.5rem",
112 | 11: "2.75rem",
113 | 12: "3rem",
114 | 14: "3.5rem",
115 | 16: "4rem",
116 | },
117 | },
118 | };
119 | ```
120 |
121 |
122 |
123 | ---
124 |
125 | ### 3. Control box-shadow **`spread`**
126 |
127 | **Class Syntax**: `shadow-spread-{theme.boxShadowSpread}`
128 |
129 | **Description**: Expands or contracts the shadow surface area omnidirectionally
130 |
131 | **Examples**:
132 |
133 | - `shadow-spread-1` (expands the shadow by `theme.boxShadowSpread.1` units),
134 | - `shadow-spread-2` (expands the shadow by `theme.boxShadowSpread.2` units),
135 | - `-shadow-spread-px` (contracts the shadow by `1px`),
136 | - `-shadow-spread-[3px]` (contracts the shadow by `3px` via arbitrary values syntax)
137 |
138 | **Configure**: Override/extend spread classes/values via `tailwind.config.js` > `theme` > `extend` > `boxShadowSpread`.
139 |
140 |
141 | Default Theme Values:
142 | `theme.boxShadowSpread` defaults to:
143 |
144 | ```js
145 | module.exports = {
146 | /* ... */
147 | theme: {
148 | boxShadowSpread: {
149 | px: "1px",
150 | 0: "0",
151 | 0.5: "0.125rem",
152 | 1: "0.25rem",
153 | 1.5: "0.375rem",
154 | 2: "0.5rem",
155 | 2.5: "0.625rem",
156 | 3: "0.75rem",
157 | 3.5: "0.875rem",
158 | 4: "1rem",
159 | },
160 | },
161 | };
162 | ```
163 |
164 |
165 |
166 | ---
167 |
168 | ### 4. Control box-shadow **`opacity`**
169 |
170 | **Class Syntax**: `shadow-opacity-{theme.boxShadowOpacity}`
171 |
172 | **Description**: Shadow colors are still controlled by the built-in `shadow-{color}/{opacity}` classes; however, there are scenarios where you may wish to override the opacity without redeclaring the color, in which case you can use the new `shadow-opacity-*` class.
173 |
174 | **Examples**:
175 |
176 | - `shadow-opacity-15` (sets shadow color opacity to `0.15`),
177 | - `shadow-opacity-0` (sets shadow color opacity to `0`, i.e. fully transparent),
178 | - `shadow-opacity-100` (sets shadow color opacity to `1`, i.e. fully opaque),
179 |
180 | **Configure**: Override/extend shadow opacity classes/values via `tailwind.config.js` > `theme` > `extend` > `boxShadowOpacity`.
181 |
182 |
183 | Default Theme Values:
184 | `theme.boxShadowOpacity` defaults to:
185 |
186 | ```js
187 | module.exports = {
188 | /* ... */
189 | theme: {
190 | boxShadowOpacity: {
191 | 0: "0",
192 | 5: "5",
193 | 10: "10",
194 | 15: "15",
195 | /* ... */
196 | 100: "100",
197 | },
198 | },
199 | };
200 | ```
201 |
202 |
203 |
204 | ---
205 |
206 | > [!NOTE]
207 | > Tailwind's built-in `shadow-{size}` classes continue to work as is, applying their own default offset + blur + spread values. When present, the new offset/blur/spread classes simply override those defaults. A `shadow-{size}` class is actually still required to be used alongside the offset/blur/spread classes, otherwise the `box-shadow` property won't be set.
208 |
209 | ## Shadow layering/stacking
210 |
211 | Tailwind Extended Shadows provides a few utility classes to auto-generate shadow "layers" (i.e. shadows stacked on top of each other); layering shadows can help you achieve more realistic, smooth, and/or sharp shadows -- [here's a good article](https://tobiasahlin.com/blog/layered-smooth-box-shadows/) that demonstrates its power.
212 |
213 | ### 1. Control the number of layers
214 |
215 | **Class Syntax**: `shadows-{theme.boxShadowLayers}`
216 |
217 | **Description**: Auto-generates the number of shadow layers specified (theme options default to between 2 and 8 layers). You must specify a "base" shadow using the built-in Tailwind shadow classes (optionally using the `offset`/`blur`/`spread`/`opacity` utilities described above); the additional shadow layers will be auto-generated based on the "base" shadow (with pure CSS, thanks to a combination of CSS custom properties + `calc()`).
218 |
219 | **Examples**:
220 |
221 | - `shadows-3` (generates 2 shadow layers in addition to the "base" layer),
222 | - `shadows-5` (generates 4 shadow layers in addition to the "base" layer),
223 |
224 | **Configure**: Override/extend `shadows-*` classes/values via `tailwind.config.js` > `theme` > `extend` > `boxShadowLayers`.
225 |
226 |
227 | Default Theme Values:
228 | `theme.boxShadowLayers` defaults to:
229 |
230 | ```js
231 | module.exports = {
232 | /* ... */
233 | theme: {
234 | boxShadowLayers: {
235 | 2: "2",
236 | 3: "3",
237 | 4: "4",
238 | 5: "5",
239 | 6: "6",
240 | 7: "7",
241 | 8: "8",
242 | },
243 | },
244 | };
245 | ```
246 |
247 |
248 |
249 | ---
250 |
251 | ### 2. Control how the layers "scale"
252 |
253 | **Class Syntax**: `shadows-scale-{theme.boxShadowLayersScale}`
254 |
255 | **Description**: By default, each generated layer uses the same `x`/`y`/`blur`/`spread` values as the "base" shadow -- i.e. the base shadow is simply repeated on top of itself, which isn't usually ideal. The `shadows-scale-*` utility provides a way to specify a "multiplier" to generate each layer's `x`/`y`/`blur` in a way that scales from smallest to biggest (note: `spread` stays the same across all layers, as scaling this value is almost never desirable in my experience).
256 |
257 | **Examples**:
258 |
259 | - `shadows-scale-2` -- multiplies the base `x`/`y`/`blur` values by `2` to the power of the current layer number; example output:
260 |
261 | ```js
262 | // using layer utilities "shadows-5 shadows-scale-2":
263 | 0px 1px 1px 0px rgb(0 0 0 / 0.1) // base values
264 | 0px 2px 2px 0px rgb(0 0 0 / 0.1) // base values * 2^1
265 | 0px 4px 4px 0px rgb(0 0 0 / 0.1) // base values * 2^2
266 | 0px 8px 8px 0px rgb(0 0 0 / 0.1) // base values * 2^3
267 | 0px 16px 16px 0px rgb(0 0 0 / 0.1) // base values * 2^4
268 | ```
269 |
270 | **Configure**: Override/extend `shadows-scale-*` classes/values via `tailwind.config.js` > `theme` > `extend` > `boxShadowLayersScale`.
271 |
272 |
273 | Default Theme Values:
274 | `theme.boxShadowLayersScale` defaults to:
275 |
276 | ```js
277 | module.exports = {
278 | /* ... */
279 | theme: {
280 | boxShadowLayersScale: {
281 | 1: "1",
282 | 1.25: "1.25",
283 | 1.5: "1.5",
284 | 1.75: "1.75",
285 | /* ... */
286 | 4.75: "4.75",
287 | 5: "5",
288 | },
289 | },
290 | };
291 | ```
292 |
293 |
294 |
295 | ---
296 |
297 | ### 3. Apply easing function to adjust how layers "scale"
298 |
299 | **Class Syntax**: `shadows-ease-{in,out}`
300 |
301 | **Description**: In addition to `shadows-scale-*`, you can specify an easing function to inject into the scaling math. This allows the shadow layers to scale in a more fluid/less linear way. Currently only supports "quadratic" easing (due to limitations in CSS' ability to do complex math).
302 |
303 | **Examples**:
304 |
305 | - `shadows-ease-in` -- scales the shadow layers starting slowly and accelerating towards the end.
306 | - `shadows-ease-out` -- scales the shadow layers starting fast and decelerating towards the end.
307 |
308 | **Configure**: Unfortunately this class group is not configurable via your Tailwind theme, as it requires writing unique JS for each variation to ensure proper easing math is applied.
309 |
310 | ---
311 |
312 | ### Layering Tips
313 |
314 | - Adding layers darkens your shadows -- to counteract this, reduce your base shadow color opacity
315 | - Because layer scaling is based on the "base" shadow values, you'll usually want to keep your base shadow values on the small side; i.e. use `shadow-sm` rather than `shadow-xl` when pairing it with `shadows-{2-8}`
316 | - Sometimes there's no visible difference when applying the `shadows-ease-{in,out}` classes; their effect becomes more apparent when using higher base offset/blur and/or scaling values
317 |
318 | ## Playground
319 |
320 | Use the following Tailwind Playground to quickly test out these new shadow classes in real-time: https://play.tailwindcss.com/6rFqo93e6h
321 |
322 | ## CSS Output:
323 |
324 | Default output without `tailwind-extended-shadows` installed:
325 |
326 | ```css
327 | .shadow-lg {
328 | --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
329 | --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px
330 | var(--tw-shadow-color);
331 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(
332 | --tw-ring-shadow,
333 | 0 0 #0000
334 | ), var(--tw-shadow);
335 | }
336 | .shadow-slate-900\/15 {
337 | --tw-shadow-color: rgb(15 23 42 / 0.15);
338 | --tw-shadow: var(--tw-shadow-colored);
339 | }
340 | ```
341 |
342 | With `tailwind-extended-shadows` installed:
343 |
344 | ```css
345 | .shadow-lg {
346 | /* The following CSS properties use the .shadow-lg default values */
347 | --tw-shadow-x-offset: 0px;
348 | --tw-shadow-y-offset: 4px;
349 | --tw-shadow-blur: 6px;
350 | --tw-shadow-spread: -4px;
351 | --tw-shadow-opacity: 1;
352 | --tw-shadow-layers: 0 0 #0000;
353 | --tw-shadows-multiplier: 1;
354 | --tw-shadow-layer-base: 0px 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 /
355 | 0.1)), var(--tw-shadow-x-offset) var(--tw-shadow-y-offset) var(
356 | --tw-shadow-blur
357 | ) var(--tw-shadow-spread) var(--tw-shadow-color, rgb(0 0 0 / 0.1));
358 | --tw-shadow: var(--tw-shadow-layer-base);
359 | box-shadow: var(--tw-inset-shadow, 0 0 #0000), var(
360 | --tw-ring-offset-shadow,
361 | 0 0 #0000
362 | ), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
363 | }
364 | .shadow-slate-900\/15 {
365 | /* adds support for opacity and doesn't set `--tw-shadow` anymore due to re-structure */
366 | --tw-shadow-color: rgb(15 23 42 / var(--tw-shadow-opacity, 0.15));
367 | --tw-shadow-opacity: 0.15;
368 | }
369 | .shadow-y-2 {
370 | /* overrides the `--tw-shadow-y-offset` value set by `shadow-lg` */
371 | --tw-shadow-y-offset: 0.5rem;
372 | }
373 | .shadow-x-2 {
374 | /* overrides the `--tw-shadow-x-offset` value set by `shadow-lg` */
375 | --tw-shadow-x-offset: 0.5rem;
376 | }
377 | .-shadow-spread-2 {
378 | /* overrides the `--tw-shadow-spread` value set by `shadow-lg` */
379 | --tw-shadow-spread: -0.5rem;
380 | }
381 | .shadow-blur-4 {
382 | /* overrides the `--tw-shadow-blur` value set by `shadow-lg` */
383 | --tw-shadow-blur: 1rem;
384 | }
385 | .shadows-4 {
386 | --tw-shadows-multiplier: 1;
387 | --tw-shadow-layers: calc(
388 | var(--tw-shadow-x-offset) * var(--tw-shadows-multiplier)
389 | ) calc(var(--tw-shadow-y-offset) * var(--tw-shadows-multiplier)) calc(
390 | var(--tw-shadow-blur) * var(--tw-shadows-multiplier)
391 | ) var(--tw-shadow-spread) var(--tw-shadow-color, rgb(0 0 0 / 0.1)), calc(
392 | var(--tw-shadow-x-offset) * var(--tw-shadows-multiplier) * var(--tw-shadows-multiplier)
393 | ) calc(
394 | var(--tw-shadow-y-offset) * var(--tw-shadows-multiplier) * var(--tw-shadows-multiplier)
395 | )
396 | calc(
397 | var(--tw-shadow-blur) * var(--tw-shadows-multiplier) * var(--tw-shadows-multiplier)
398 | ) var(--tw-shadow-spread) var(--tw-shadow-color, rgb(0 0 0 / 0.1)),
399 | calc(
400 | var(--tw-shadow-x-offset) * var(--tw-shadows-multiplier) * var(
401 | --tw-shadows-multiplier
402 | ) * var(--tw-shadows-multiplier)
403 | ) calc(
404 | var(--tw-shadow-y-offset) * var(--tw-shadows-multiplier) * var(
405 | --tw-shadows-multiplier
406 | ) * var(--tw-shadows-multiplier)
407 | )
408 | calc(
409 | var(--tw-shadow-blur) * var(--tw-shadows-multiplier) * var(
410 | --tw-shadows-multiplier
411 | ) * var(--tw-shadows-multiplier)
412 | ) var(--tw-shadow-spread) var(--tw-shadow-color, rgb(0 0 0 / 0.1));
413 | --tw-shadow: var(--tw-shadow-layer-base), var(--tw-shadow-layers);
414 | }
415 | .shadows-4.shadows-ease-in {
416 | /* overrides the `--tw-shadow-layers` value set by `shadows-4`, applying extra "ease-in" math */
417 | --tw-shadow-layers: calc(
418 | calc(var(--tw-shadow-x-offset) * var(--tw-shadows-multiplier)) * 0.25 *
419 | 0.25
420 | ) calc(
421 | calc(var(--tw-shadow-y-offset) * var(--tw-shadows-multiplier)) * 0.25 *
422 | 0.25
423 | )
424 | calc(
425 | calc(var(--tw-shadow-blur) * var(--tw-shadows-multiplier)) * 0.25 * 0.25
426 | ) var(--tw-shadow-spread) var(--tw-shadow-color, rgb(0 0 0 / 0.1)), calc(
427 | calc(
428 | var(--tw-shadow-x-offset) * var(--tw-shadows-multiplier) * var(--tw-shadows-multiplier)
429 | ) * 0.5 * 0.5
430 | ) calc(
431 | calc(
432 | var(--tw-shadow-y-offset) * var(--tw-shadows-multiplier) * var(--tw-shadows-multiplier)
433 | ) * 0.5 * 0.5
434 | )
435 | calc(
436 | calc(
437 | var(--tw-shadow-blur) * var(--tw-shadows-multiplier) * var(--tw-shadows-multiplier)
438 | ) * 0.5 * 0.5
439 | ) var(--tw-shadow-spread) var(--tw-shadow-color, rgb(0 0 0 / 0.1)),
440 | calc(
441 | calc(
442 | var(--tw-shadow-x-offset) * var(--tw-shadows-multiplier) * var(
443 | --tw-shadows-multiplier
444 | ) * var(--tw-shadows-multiplier)
445 | ) * 0.75 * 0.75
446 | ) calc(
447 | calc(
448 | var(--tw-shadow-y-offset) * var(--tw-shadows-multiplier) * var(
449 | --tw-shadows-multiplier
450 | ) * var(--tw-shadows-multiplier)
451 | ) * 0.75 * 0.75
452 | )
453 | calc(
454 | calc(
455 | var(--tw-shadow-blur) * var(--tw-shadows-multiplier) * var(
456 | --tw-shadows-multiplier
457 | ) * var(--tw-shadows-multiplier)
458 | ) * 0.75 * 0.75
459 | ) var(--tw-shadow-spread) var(--tw-shadow-color, rgb(0 0 0 / 0.1));
460 | }
461 | .shadows-scale-3 {
462 | /* overrides the `--tw-shadows-multiplier` value set by `shadows-4` */
463 | --tw-shadows-multiplier: 3;
464 | }
465 | ```
466 |
467 | As you can see, Tailwind Extended Shadows will increase the size of your CSS ouput, especially when using a large amount of layers combined with the easing utilities -- but it's arguably a negligible difference in the grand scheme of things.
468 |
469 | ## Install
470 |
471 | ```bash
472 | npm i tailwind-extended-shadows
473 | ```
474 |
475 | Then add the plugin to your `tailwind.config.js`:
476 |
477 | ```js
478 | // tailwind.config.js
479 | module.exports = {
480 | /* --- */
481 | plugins: [require("tailwind-extended-shadows")],
482 | };
483 | ```
484 |
485 | ### `tailwind-merge` compatibility plugin
486 |
487 | If you're using the wonderful `tailwind-merge` package to take care of removing conflicting Tailwind classes at runtime, make sure to use our `withExtendedShadows` compatibility plugin from the separate [`tailwind-extended-shadows-merge`](https://github.com/kaelansmith/tailwind-extended-shadows-merge) package; otherwise, the extra shadow utility classes will be considered conflicting and will get stripped out when they shouldn't.
488 |
489 | ```js
490 | import { extendTailwindMerge } from "tailwind-merge";
491 | import { withExtendedShadows } from "tailwind-extended-shadows-merge";
492 |
493 | export const twMerge = extendTailwindMerge(withExtendedShadows);
494 | ```
495 |
496 | ---
497 |
498 | ### Made by Kaelan Smith
499 |
500 | - [Personal Website](https://kaelansmith.com)
501 | - [Twitter/X](https://twitter.com/kaelancsmith)
502 | - [Github](https://github.com/kaelansmith/)
503 |
--------------------------------------------------------------------------------
/dist/helpers.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets the underlying default import of a module.
3 | *
4 | * This is used to handle internal imoprts from Tailwind, since Tailwind Play
5 | * handles these imports differently.
6 | *
7 | * @template T
8 | * @param {T | { __esModule: unknown, default: T }} mod The module
9 | * @returns {T} The bare export
10 | */
11 | declare const importDefault: (mod: any) => any;
12 |
--------------------------------------------------------------------------------
/dist/helpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets the underlying default import of a module.
3 | *
4 | * This is used to handle internal imoprts from Tailwind, since Tailwind Play
5 | * handles these imports differently.
6 | *
7 | * @template T
8 | * @param {T | { __esModule: unknown, default: T }} mod The module
9 | * @returns {T} The bare export
10 | */
11 | // eslint-disable-next-line no-underscore-dangle
12 | const importDefault = (mod) => (mod && mod.__esModule ? mod.default : mod);
13 | module.exports = {
14 | importDefault,
15 | };
16 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | export { extendedShadowsPlugin as default } from "./plugin";
2 |
--------------------------------------------------------------------------------
/dist/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.default = void 0;
4 | var plugin_1 = require("./plugin");
5 | Object.defineProperty(exports, "default", { enumerable: true, get: function () { return plugin_1.extendedShadowsPlugin; } });
6 | // const { withExtendedShadows } = require("./twMergePlugin");
7 | // const { extendedShadowsPlugin } = require("./plugin");
8 | // module.exports = {
9 | // extendedShadowsPlugin,
10 | // withExtendedShadows,
11 | // };
12 |
--------------------------------------------------------------------------------
/dist/plugin.d.ts:
--------------------------------------------------------------------------------
1 | export declare const extendedShadowsPlugin: any;
2 |
--------------------------------------------------------------------------------
/dist/plugin.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.extendedShadowsPlugin = void 0;
4 | const plugin = require("tailwindcss/plugin");
5 | // @ts-ignore
6 | const flattenColorPaletteImport = require("tailwindcss/lib/util/flattenColorPalette");
7 | const { parseColor } = require("tailwindcss/lib/util/color");
8 | const { importDefault } = require("./helpers");
9 | // Tailwind Play will import these internal imports as ES6 imports, while most
10 | // other workflows will import them as CommonJS imports.
11 | const flattenColorPalette = importDefault(flattenColorPaletteImport);
12 | // Note: we purposely keep all plugin code in one file to make it easy to copy/paste between Tailwind Playground (which serves as a nice dev/testing environment)
13 | const shadowScaleDefaultTheme = {};
14 | for (let i = 1; i <= 5; i += 0.25) {
15 | shadowScaleDefaultTheme[i] = i.toString();
16 | }
17 | const shadowOpacityDefaultTheme = {};
18 | for (let i = 0; i <= 100; i += 5) {
19 | shadowOpacityDefaultTheme[i] = i.toString();
20 | }
21 | exports.extendedShadowsPlugin = plugin(function ({ matchUtilities, theme }) {
22 | /**
23 | * Helper that parses comma-separated box-shadow values into array of objects,
24 | * making it easy to loop over and extract x/y/blur/spread/color
25 | */
26 | const parseShadowValue = (shadowVal) => {
27 | // Regular expression to extract x-offset, y-offset, blur, spread, and color
28 | const shadowRegex = /([-\d.]+[a-z]*) ([-\d.]+[a-z]*) ([-\d.]+[a-z]*) ([-\d.]+[a-z]*) (rgba?\([^)]+\)|#[\dA-Fa-f]+)/g;
29 | const shadows = [];
30 | let matches;
31 | while ((matches = shadowRegex.exec(shadowVal)) !== null) {
32 | let [, x, y, blur, spread, color] = matches;
33 | [x, y, blur, spread] = [x, y, blur, spread].map((value) => value === "0" ? "0px" : value);
34 | shadows.push({ x, y, blur, spread, color });
35 | }
36 | return shadows.length > 0 ? shadows : null;
37 | };
38 | /**
39 | * Override built-in shadow-{size} utilities to incorporate custom offset +
40 | * blur + spread CSS properties, with fallbacks to its default values.
41 | * Note: it's important that we do this before adding the other utilities further
42 | * below, as it affects the order of the CSS output, and offset/blur/spread needs
43 | * to come after shadow-{size} in order to override the defaults.
44 | */
45 | const shadowTheme = theme("boxShadow");
46 | matchUtilities({
47 | shadow: (value) => {
48 | const shadowValues = parseShadowValue(value);
49 | if (shadowValues?.length) {
50 | let preBaseShadowValues = "0 0 #0000";
51 | let lastBaseShadowValue = shadowValues[0];
52 | if (shadowValues?.length >= 2) {
53 | /**
54 | * If values from Tailwind's `theme.boxShadow` include multiple shadow layers,
55 | * we handle that below. The last layer becomes the "base" layer used for
56 | * auto-generating additional layers via the `shadows-{2-8}` utility.
57 | */
58 | preBaseShadowValues = "";
59 | lastBaseShadowValue = shadowValues.pop();
60 | shadowValues.forEach(({ x, y, blur, spread, color }, i) => {
61 | if (i > 0)
62 | preBaseShadowValues += ", ";
63 | preBaseShadowValues += `${x} ${y} ${blur} ${spread} var(--tw-shadow-color, ${color})`;
64 | });
65 | }
66 | const { x, y, blur, spread, color } = lastBaseShadowValue;
67 | return {
68 | /**
69 | * Note: we set defaults for offset/blur/spread/opacity/layers/multiplier/ease variables here to avoid inheriting
70 | * from one of their respective utility classes higher up the tree. If one of these utility classes gets applied
71 | * alongside a shadow-{size} class, it will override the defaults because those classes get output after the shadow-{size} classes.
72 | */
73 | "--tw-shadow-x-offset": x,
74 | "--tw-shadow-y-offset": y,
75 | "--tw-shadow-blur": blur,
76 | "--tw-shadow-spread": spread,
77 | "--tw-shadow-opacity": "1",
78 | "--tw-shadow-layers": "0 0 #0000",
79 | "--tw-shadows-multiplier": "1",
80 | "--tw-shadow-layer-base": `${preBaseShadowValues}, var(--tw-shadow-x-offset) var(--tw-shadow-y-offset) var(--tw-shadow-blur) var(--tw-shadow-spread) var(--tw-shadow-color, ${color})`,
81 | "--tw-shadow": "var(--tw-shadow-layer-base)",
82 | "box-shadow": `var(--tw-inset-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)`,
83 | };
84 | }
85 | },
86 | }, {
87 | values: shadowTheme,
88 | type: "shadow",
89 | });
90 | /* Converts HEX color to RGB */
91 | const toRGB = (value) => parseColor(value)?.color.join(" ");
92 | const isHexColor = (color) => {
93 | const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
94 | return hexColorRegex.test(color);
95 | };
96 | const themeShadowColors = theme("boxShadowColor");
97 | matchUtilities({
98 | shadow: (value, { modifier }) => {
99 | const defaultOpacityValue = modifier === null || modifier === undefined
100 | ? 1
101 | : parseInt(modifier) / 100;
102 | const opacityValue = `var(--tw-shadow-opacity, ${defaultOpacityValue})`;
103 | const color = isHexColor(value)
104 | ? `rgb(${toRGB(value)} / ${opacityValue})`
105 | : typeof value == "function"
106 | ? value({ opacityValue }) // handle hsl values which are functions
107 | : value;
108 | return {
109 | "--tw-shadow-color": color,
110 | "--tw-shadow-opacity": `${defaultOpacityValue}`,
111 | // have to set "--tw-shadow" again here to override built-in Tailwind stuff
112 | "--tw-shadow": `var(--tw-shadow-layer-base), var(--tw-shadow-layers, 0 0 #0000)`,
113 | };
114 | },
115 | }, {
116 | values: flattenColorPalette(themeShadowColors),
117 | modifiers: "any",
118 | type: ["color"],
119 | });
120 | /**
121 | * Create `shadows-{2-8}` utilities for auto-generating shadow layers
122 | * Note: `shadows-ease-{in,out}` utilities are also specified here as nested properties
123 | */
124 | const layerValues = theme("boxShadowLayers");
125 | matchUtilities({
126 | shadows: (value) => {
127 | const totalIterations = parseInt(value);
128 | let layers = "";
129 | let layersEaseIn = "";
130 | let layersEaseOut = "";
131 | let multiplier = "";
132 | // note: `shadows-5` means we add 4 additional shadows to the base layer, hence `<` and not `<=` in loop:
133 | for (let i = 1; i < totalIterations; i++) {
134 | if (i > 1) {
135 | layers += ", ";
136 | layersEaseIn += ", ";
137 | layersEaseOut += ", ";
138 | multiplier += " * ";
139 | }
140 | multiplier += "var(--tw-shadows-multiplier)";
141 | const x = `calc(var(--tw-shadow-x-offset) * ${multiplier})`;
142 | const y = `calc(var(--tw-shadow-y-offset) * ${multiplier})`;
143 | const blur = `calc(var(--tw-shadow-blur) * ${multiplier})`;
144 | const end = "var(--tw-shadow-spread) var(--tw-shadow-color, rgb(0 0 0 / 0.1))";
145 | layers += `${x} ${y} ${blur} ${end}`;
146 | let multiplierEaseIn = i / totalIterations;
147 | let multiplierEaseOut = (1 - i) / totalIterations;
148 | const props = [x, y, blur];
149 | props.forEach((val) => {
150 | layersEaseIn += `calc(${val} * ${multiplierEaseIn} * ${multiplierEaseIn}) `;
151 | layersEaseOut += `calc(${val} * (1 - (${multiplierEaseOut} * ${multiplierEaseOut}))) `;
152 | });
153 | layersEaseIn += end;
154 | layersEaseOut += end;
155 | }
156 | return {
157 | "--tw-shadows-multiplier": "1",
158 | "--tw-shadow-layers": layers,
159 | "--tw-shadow": `var(--tw-shadow-layer-base), var(--tw-shadow-layers)`,
160 | "&.shadows-ease-in": {
161 | "--tw-shadow-layers": layersEaseIn,
162 | },
163 | "&.shadows-ease-out": {
164 | "--tw-shadow-layers": layersEaseOut,
165 | },
166 | };
167 | },
168 | }, {
169 | values: layerValues,
170 | });
171 | /**
172 | * Create box-shadow offset utilities:
173 | */
174 | const offsetValues = theme("boxShadowOffset");
175 | matchUtilities({
176 | "shadow-x": (value) => ({
177 | "--tw-shadow-x-offset": `${value}`,
178 | }),
179 | "shadow-y": (value) => ({
180 | "--tw-shadow-y-offset": `${value}`,
181 | }),
182 | }, {
183 | values: offsetValues,
184 | supportsNegativeValues: true,
185 | });
186 | /**
187 | * Create box-shadow blur utilities:
188 | */
189 | const blurValues = theme("boxShadowBlur");
190 | matchUtilities({
191 | "shadow-blur": (value) => ({
192 | "--tw-shadow-blur": `${value}`,
193 | }),
194 | }, {
195 | values: blurValues,
196 | });
197 | /**
198 | * Create box-shadow spread utilities:
199 | */
200 | const spreadValues = theme("boxShadowSpread");
201 | matchUtilities({
202 | "shadow-spread": (value) => ({
203 | "--tw-shadow-spread": `${value}`,
204 | }),
205 | }, {
206 | values: spreadValues,
207 | supportsNegativeValues: true,
208 | });
209 | /**
210 | * Create box-shadow opacity utilities:
211 | */
212 | const opacityValues = theme("boxShadowOpacity");
213 | matchUtilities({
214 | "shadow-opacity": (value) => ({
215 | "--tw-shadow-opacity": `${parseInt(value) / 100}`,
216 | }),
217 | }, {
218 | values: opacityValues,
219 | });
220 | /**
221 | * Create box-shadow layers scaling/multiplier utilities:
222 | */
223 | const scaleValues = theme("boxShadowLayersScale");
224 | matchUtilities({
225 | "shadows-scale": (value) => ({
226 | "--tw-shadows-multiplier": `${value}`,
227 | }),
228 | }, {
229 | values: scaleValues,
230 | supportsNegativeValues: true,
231 | });
232 | }, {
233 | // default theme values:
234 | theme: {
235 | boxShadowOffset: {
236 | px: "1px",
237 | 0: "0",
238 | 0.5: "0.125rem",
239 | 1: "0.25rem",
240 | 1.5: "0.375rem",
241 | 2: "0.5rem",
242 | 2.5: "0.625rem",
243 | 3: "0.75rem",
244 | 3.5: "0.875rem",
245 | 4: "1rem",
246 | 5: "1.25rem",
247 | 6: "1.5rem",
248 | 7: "1.75rem",
249 | 8: "2rem",
250 | },
251 | boxShadowBlur: {
252 | px: "1px",
253 | 0: "0",
254 | 0.5: "0.125rem",
255 | 1: "0.25rem",
256 | 1.5: "0.375rem",
257 | 2: "0.5rem",
258 | 2.5: "0.625rem",
259 | 3: "0.75rem",
260 | 3.5: "0.875rem",
261 | 4: "1rem",
262 | 5: "1.25rem",
263 | 6: "1.5rem",
264 | 7: "1.75rem",
265 | 8: "2rem",
266 | 9: "2.25rem",
267 | 10: "2.5rem",
268 | 11: "2.75rem",
269 | 12: "3rem",
270 | 14: "3.5rem",
271 | 16: "4rem",
272 | },
273 | boxShadowSpread: {
274 | px: "1px",
275 | 0: "0",
276 | 0.5: "0.125rem",
277 | 1: "0.25rem",
278 | 1.5: "0.375rem",
279 | 2: "0.5rem",
280 | 2.5: "0.625rem",
281 | 3: "0.75rem",
282 | 3.5: "0.875rem",
283 | 4: "1rem",
284 | },
285 | boxShadowOpacity: shadowOpacityDefaultTheme,
286 | boxShadowLayersScale: shadowScaleDefaultTheme,
287 | boxShadowLayers: {
288 | 2: "2",
289 | 3: "3",
290 | 4: "4",
291 | 5: "5",
292 | 6: "6",
293 | 7: "7",
294 | 8: "8",
295 | },
296 | },
297 | });
298 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tailwind-extended-shadows",
3 | "version": "0.4.1",
4 | "description": "TailwindCSS utility classes for fine-grain control over box-shadows, including layers.",
5 | "main": "dist/index",
6 | "types": "dist/index.d.ts",
7 | "exports": {
8 | ".": {
9 | "require": "./dist/index.js",
10 | "import": "./dist/index.js",
11 | "types": "./dist/index.d.ts"
12 | }
13 | },
14 | "scripts": {
15 | "dev": "tsc -p tsconfig.json -w --preserveWatchOutput",
16 | "build": "npm run build-ts",
17 | "build-ts": "tsc -p tsconfig.json",
18 | "clean": "rm -rf .turbo && rm -rf dist && npm run clean:modules",
19 | "clean:modules": "rm -rf node_modules"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/kaelansmith/tailwind-extended-shadows.git"
24 | },
25 | "keywords": [
26 | "tailwind",
27 | "tailwindcss",
28 | "tailwindCSS",
29 | "shadow",
30 | "box-shadow",
31 | "offset",
32 | "direction",
33 | "spread"
34 | ],
35 | "author": "Kaelan Smith",
36 | "license": "LGPL-3.0-only",
37 | "bugs": {
38 | "url": "https://github.com/kaelansmith/tailwind-extended-shadows/issues"
39 | },
40 | "homepage": "https://github.com/kaelansmith/tailwind-extended-shadows#readme",
41 | "devDependencies": {
42 | "@types/node": "^18.18.1",
43 | "tailwindcss": "^3.3.3",
44 | "tsc-watch": "^5.0.3",
45 | "typescript": "^5.3.2"
46 | },
47 | "peerDependencies": {
48 | "tailwindcss": ">=3.0.0"
49 | },
50 | "publishConfig": {
51 | "access": "public"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/playground/README.md:
--------------------------------------------------------------------------------
1 | This folder simply serves as a backup of the HTML + CSS in our Tailwind Playground, just in case it accidentally gets deleted/lost. This code isn't connected to the real playground in any way, just copy-pasted.
2 |
--------------------------------------------------------------------------------
/playground/markup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Tailwind Extended Shadows
4 |
Try adjusting the extra shadow utility classes on the cards below to see what's possible.
5 | Learn more about tailwind-extended-shadows below:
6 |
9 |
10 |
11 |
12 |
Controlling Offset
13 |
14 |
15 |
16 |
Default:
17 | shadow-lg
18 |
19 |
20 |
21 |
22 |
23 | shadow-lg
24 | shadow-y-5
25 |
26 |
27 |
28 |
29 |
30 | shadow-lg
31 | shadow-y-5
32 | shadow-x-2
33 |
34 |
35 |
36 |
37 |
Controlling Spread
38 |
39 |
40 |
41 |
Default:
42 | shadow-lg
43 |
44 |
45 |
46 |
47 |
48 | shadow-lg
49 | -shadow-spread-2
50 |
51 |
52 |
53 |
54 |
55 | shadow-lg
56 | shadow-spread-2
57 |
58 |
59 |
60 |
61 |
Controlling Layers
62 |
63 |
64 |
65 |
Default:
66 | shadow-lg
67 |
68 |
69 |
70 |
71 |
72 | shadow-lg
73 | shadows-4
74 |
75 |
76 |
77 |
78 |
79 | shadow-lg
80 | shadows-8
81 |
82 |
83 |
84 |
85 |
Note how adding layers darkens shadows; you typically want to
86 | counter-act this by reducing base-layer opacity:
87 |
88 |
89 |
90 |
Default:
91 | shadow-lg
92 |
93 |
94 |
95 |
96 |
97 | shadow-lg
98 | shadows-4
99 | shadow-opacity-10
100 |
101 |
102 |
103 |
104 |
105 | shadow-lg
106 | shadows-8
107 | shadow-opacity-5
108 |
109 |
110 |
111 |
112 |
113 |
Apply Layer Scaling
114 |
Note how adding layers and reducing opacity doesn't really change
115 | anything -- this is where layer scaling comes in:
116 |
117 |
118 |
119 |
Default:
120 | shadow-lg
121 |
122 |
123 |
124 |
125 |
126 |
127 | shadow-sm
128 | shadows-4
129 | shadow-opacity-5
130 | shadow-y-[3px]
131 | shadows-scale-3
132 |
133 |
134 |
135 |
137 |
138 | shadow-sm
139 | shadows-7
140 | shadow-opacity-5
141 | shadow-y-[3px]
142 | -shadow-spread-[10px]
143 | shadows-scale-1.5
144 |
145 |
146 |
147 |
148 |
Apply Easing to Layer Scaling
149 |
Finally, you may want to apply easing for a more fluid/less linear
150 | scaling effect.
151 |
152 |
153 |
154 |
Default:
155 | shadow-lg
156 |
157 |
158 |
159 |
160 |
162 |
163 | shadow-sm
164 | shadows-4
165 | shadow-opacity-5
166 | shadow-y-[3px]
167 | shadows-scale-3
168 | shadows-ease-in
169 |
170 |
171 |
172 |
174 |
175 | shadow-sm
176 | shadows-4
177 | shadow-opacity-5
178 | shadow-y-[3px]
179 | shadows-scale-3
180 | shadows-ease-out
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/playground/styles.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | pre {
7 | @apply text-blue-800 bg-blue-50 py-0.5 px-2 leading-none tracking-tight rounded border border-blue-200 text-sm xl:text-base shadow-sm shadow-slate-900/20;
8 | }
9 |
10 | h1 {
11 | @apply text-4xl font-bold text-slate-900 mb-4;
12 | }
13 |
14 | h2 {
15 | @apply text-3xl font-semibold text-slate-900 mb-8;
16 | }
17 |
18 | h3 {
19 | @apply flex flex-wrap gap-2 text-base md:text-lg font-semibold text-slate-800;
20 | }
21 |
22 | .card-grid {
23 | @apply mb-20 grid grid-cols-3 gap-4 md:gap-6 w-full;
24 | }
25 |
26 | .card {
27 | @apply p-4 md:p-6 max-w-sm rounded-md bg-white border min-h-56 border-slate-900/10;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets the underlying default import of a module.
3 | *
4 | * This is used to handle internal imoprts from Tailwind, since Tailwind Play
5 | * handles these imports differently.
6 | *
7 | * @template T
8 | * @param {T | { __esModule: unknown, default: T }} mod The module
9 | * @returns {T} The bare export
10 | */
11 | // eslint-disable-next-line no-underscore-dangle
12 | const importDefault = (mod) => (mod && mod.__esModule ? mod.default : mod);
13 |
14 | module.exports = {
15 | importDefault,
16 | };
17 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { extendedShadowsPlugin as default } from "./plugin";
2 | // const { withExtendedShadows } = require("./twMergePlugin");
3 | // const { extendedShadowsPlugin } = require("./plugin");
4 |
5 | // module.exports = {
6 | // extendedShadowsPlugin,
7 | // withExtendedShadows,
8 | // };
9 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | const plugin = require("tailwindcss/plugin");
2 | // @ts-ignore
3 | const flattenColorPaletteImport = require("tailwindcss/lib/util/flattenColorPalette");
4 | const { parseColor } = require("tailwindcss/lib/util/color");
5 | const { importDefault } = require("./helpers");
6 |
7 | // Tailwind Play will import these internal imports as ES6 imports, while most
8 | // other workflows will import them as CommonJS imports.
9 | const flattenColorPalette = importDefault(flattenColorPaletteImport);
10 |
11 | // Note: we purposely keep all plugin code in one file to make it easy to copy/paste between Tailwind Playground (which serves as a nice dev/testing environment)
12 |
13 | const shadowScaleDefaultTheme = {};
14 | for (let i = 1; i <= 5; i += 0.25) {
15 | shadowScaleDefaultTheme[i] = i.toString();
16 | }
17 |
18 | const shadowOpacityDefaultTheme = {};
19 | for (let i = 0; i <= 100; i += 5) {
20 | shadowOpacityDefaultTheme[i] = i.toString();
21 | }
22 |
23 | export const extendedShadowsPlugin = plugin(
24 | function ({ matchUtilities, theme }) {
25 | /**
26 | * Helper that parses comma-separated box-shadow values into array of objects,
27 | * making it easy to loop over and extract x/y/blur/spread/color
28 | */
29 | const parseShadowValue = (shadowVal) => {
30 | // Regular expression to extract x-offset, y-offset, blur, spread, and color
31 | const shadowRegex =
32 | /([-\d.]+[a-z]*) ([-\d.]+[a-z]*) ([-\d.]+[a-z]*) ([-\d.]+[a-z]*) (rgba?\([^)]+\)|#[\dA-Fa-f]+)/g;
33 |
34 | const shadows = [];
35 | let matches;
36 |
37 | while ((matches = shadowRegex.exec(shadowVal)) !== null) {
38 | let [, x, y, blur, spread, color] = matches;
39 |
40 | [x, y, blur, spread] = [x, y, blur, spread].map((value) =>
41 | value === "0" ? "0px" : value
42 | );
43 |
44 | shadows.push({ x, y, blur, spread, color });
45 | }
46 |
47 | return shadows.length > 0 ? shadows : null;
48 | };
49 |
50 | /**
51 | * Override built-in shadow-{size} utilities to incorporate custom offset +
52 | * blur + spread CSS properties, with fallbacks to its default values.
53 | * Note: it's important that we do this before adding the other utilities further
54 | * below, as it affects the order of the CSS output, and offset/blur/spread needs
55 | * to come after shadow-{size} in order to override the defaults.
56 | */
57 |
58 | const shadowTheme = theme("boxShadow");
59 |
60 | matchUtilities(
61 | {
62 | shadow: (value) => {
63 | const shadowValues = parseShadowValue(value);
64 |
65 | if (shadowValues?.length) {
66 | let preBaseShadowValues = "0 0 #0000";
67 | let lastBaseShadowValue = shadowValues[0];
68 |
69 | if (shadowValues?.length >= 2) {
70 | /**
71 | * If values from Tailwind's `theme.boxShadow` include multiple shadow layers,
72 | * we handle that below. The last layer becomes the "base" layer used for
73 | * auto-generating additional layers via the `shadows-{2-8}` utility.
74 | */
75 |
76 | preBaseShadowValues = "";
77 | lastBaseShadowValue = shadowValues.pop();
78 | shadowValues.forEach(({ x, y, blur, spread, color }, i) => {
79 | if (i > 0) preBaseShadowValues += ", ";
80 | preBaseShadowValues += `${x} ${y} ${blur} ${spread} var(--tw-shadow-color, ${color})`;
81 | });
82 | }
83 |
84 | const { x, y, blur, spread, color } = lastBaseShadowValue;
85 |
86 | return {
87 | /**
88 | * Note: we set defaults for offset/blur/spread/opacity/layers/multiplier/ease variables here to avoid inheriting
89 | * from one of their respective utility classes higher up the tree. If one of these utility classes gets applied
90 | * alongside a shadow-{size} class, it will override the defaults because those classes get output after the shadow-{size} classes.
91 | */
92 | "--tw-shadow-x-offset": x,
93 | "--tw-shadow-y-offset": y,
94 | "--tw-shadow-blur": blur,
95 | "--tw-shadow-spread": spread,
96 | "--tw-shadow-opacity": "1",
97 | "--tw-shadow-layers": "0 0 #0000",
98 | "--tw-shadows-multiplier": "1",
99 | "--tw-shadow-layer-base": `${preBaseShadowValues}, var(--tw-shadow-x-offset) var(--tw-shadow-y-offset) var(--tw-shadow-blur) var(--tw-shadow-spread) var(--tw-shadow-color, ${color})`,
100 | "--tw-shadow": "var(--tw-shadow-layer-base)",
101 | "box-shadow": `var(--tw-inset-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)`,
102 | };
103 | }
104 | },
105 | },
106 | {
107 | values: shadowTheme,
108 | type: "shadow",
109 | }
110 | );
111 |
112 | /* Converts HEX color to RGB */
113 | const toRGB = (value) => parseColor(value)?.color.join(" ");
114 |
115 | const isHexColor = (color) => {
116 | const hexColorRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
117 | return hexColorRegex.test(color);
118 | };
119 |
120 | const themeShadowColors = theme("boxShadowColor");
121 |
122 | matchUtilities(
123 | {
124 | shadow: (value, { modifier }) => {
125 | const defaultOpacityValue =
126 | modifier === null || modifier === undefined
127 | ? 1
128 | : parseInt(modifier) / 100;
129 |
130 | const opacityValue = `var(--tw-shadow-opacity, ${defaultOpacityValue})`;
131 |
132 | const color = isHexColor(value)
133 | ? `rgb(${toRGB(value)} / ${opacityValue})`
134 | : typeof value == "function"
135 | ? value({ opacityValue }) // handle hsl values which are functions
136 | : value;
137 |
138 | return {
139 | "--tw-shadow-color": color,
140 | "--tw-shadow-opacity": `${defaultOpacityValue}`,
141 | // have to set "--tw-shadow" again here to override built-in Tailwind stuff
142 | "--tw-shadow": `var(--tw-shadow-layer-base), var(--tw-shadow-layers, 0 0 #0000)`,
143 | };
144 | },
145 | },
146 | {
147 | values: flattenColorPalette(themeShadowColors),
148 | modifiers: "any",
149 | type: ["color"],
150 | }
151 | );
152 |
153 | /**
154 | * Create `shadows-{2-8}` utilities for auto-generating shadow layers
155 | * Note: `shadows-ease-{in,out}` utilities are also specified here as nested properties
156 | */
157 |
158 | const layerValues = theme("boxShadowLayers");
159 |
160 | matchUtilities(
161 | {
162 | shadows: (value) => {
163 | const totalIterations = parseInt(value);
164 | let layers = "";
165 | let layersEaseIn = "";
166 | let layersEaseOut = "";
167 | let multiplier = "";
168 |
169 | // note: `shadows-5` means we add 4 additional shadows to the base layer, hence `<` and not `<=` in loop:
170 | for (let i = 1; i < totalIterations; i++) {
171 | if (i > 1) {
172 | layers += ", ";
173 | layersEaseIn += ", ";
174 | layersEaseOut += ", ";
175 | multiplier += " * ";
176 | }
177 |
178 | multiplier += "var(--tw-shadows-multiplier)";
179 |
180 | const x = `calc(var(--tw-shadow-x-offset) * ${multiplier})`;
181 | const y = `calc(var(--tw-shadow-y-offset) * ${multiplier})`;
182 | const blur = `calc(var(--tw-shadow-blur) * ${multiplier})`;
183 | const end =
184 | "var(--tw-shadow-spread) var(--tw-shadow-color, rgb(0 0 0 / 0.1))";
185 | layers += `${x} ${y} ${blur} ${end}`;
186 |
187 | let multiplierEaseIn = i / totalIterations;
188 | let multiplierEaseOut = (1 - i) / totalIterations;
189 |
190 | const props = [x, y, blur];
191 | props.forEach((val) => {
192 | layersEaseIn += `calc(${val} * ${multiplierEaseIn} * ${multiplierEaseIn}) `;
193 | layersEaseOut += `calc(${val} * (1 - (${multiplierEaseOut} * ${multiplierEaseOut}))) `;
194 | });
195 |
196 | layersEaseIn += end;
197 | layersEaseOut += end;
198 | }
199 |
200 | return {
201 | "--tw-shadows-multiplier": "1",
202 | "--tw-shadow-layers": layers,
203 | "--tw-shadow": `var(--tw-shadow-layer-base), var(--tw-shadow-layers)`,
204 | "&.shadows-ease-in": {
205 | "--tw-shadow-layers": layersEaseIn,
206 | },
207 | "&.shadows-ease-out": {
208 | "--tw-shadow-layers": layersEaseOut,
209 | },
210 | };
211 | },
212 | },
213 | {
214 | values: layerValues,
215 | }
216 | );
217 |
218 | /**
219 | * Create box-shadow offset utilities:
220 | */
221 |
222 | const offsetValues = theme("boxShadowOffset");
223 |
224 | matchUtilities(
225 | {
226 | "shadow-x": (value) => ({
227 | "--tw-shadow-x-offset": `${value}`,
228 | }),
229 | "shadow-y": (value) => ({
230 | "--tw-shadow-y-offset": `${value}`,
231 | }),
232 | },
233 | {
234 | values: offsetValues,
235 | supportsNegativeValues: true,
236 | }
237 | );
238 |
239 | /**
240 | * Create box-shadow blur utilities:
241 | */
242 |
243 | const blurValues = theme("boxShadowBlur");
244 |
245 | matchUtilities(
246 | {
247 | "shadow-blur": (value) => ({
248 | "--tw-shadow-blur": `${value}`,
249 | }),
250 | },
251 | {
252 | values: blurValues,
253 | }
254 | );
255 |
256 | /**
257 | * Create box-shadow spread utilities:
258 | */
259 |
260 | const spreadValues = theme("boxShadowSpread");
261 |
262 | matchUtilities(
263 | {
264 | "shadow-spread": (value) => ({
265 | "--tw-shadow-spread": `${value}`,
266 | }),
267 | },
268 | {
269 | values: spreadValues,
270 | supportsNegativeValues: true,
271 | }
272 | );
273 |
274 | /**
275 | * Create box-shadow opacity utilities:
276 | */
277 |
278 | const opacityValues = theme("boxShadowOpacity");
279 |
280 | matchUtilities(
281 | {
282 | "shadow-opacity": (value) => ({
283 | "--tw-shadow-opacity": `${parseInt(value) / 100}`,
284 | }),
285 | },
286 | {
287 | values: opacityValues,
288 | }
289 | );
290 |
291 | /**
292 | * Create box-shadow layers scaling/multiplier utilities:
293 | */
294 |
295 | const scaleValues = theme("boxShadowLayersScale");
296 |
297 | matchUtilities(
298 | {
299 | "shadows-scale": (value) => ({
300 | "--tw-shadows-multiplier": `${value}`,
301 | }),
302 | },
303 | {
304 | values: scaleValues,
305 | supportsNegativeValues: true,
306 | }
307 | );
308 | },
309 | {
310 | // default theme values:
311 | theme: {
312 | boxShadowOffset: {
313 | px: "1px",
314 | 0: "0",
315 | 0.5: "0.125rem",
316 | 1: "0.25rem",
317 | 1.5: "0.375rem",
318 | 2: "0.5rem",
319 | 2.5: "0.625rem",
320 | 3: "0.75rem",
321 | 3.5: "0.875rem",
322 | 4: "1rem",
323 | 5: "1.25rem",
324 | 6: "1.5rem",
325 | 7: "1.75rem",
326 | 8: "2rem",
327 | },
328 | boxShadowBlur: {
329 | px: "1px",
330 | 0: "0",
331 | 0.5: "0.125rem",
332 | 1: "0.25rem",
333 | 1.5: "0.375rem",
334 | 2: "0.5rem",
335 | 2.5: "0.625rem",
336 | 3: "0.75rem",
337 | 3.5: "0.875rem",
338 | 4: "1rem",
339 | 5: "1.25rem",
340 | 6: "1.5rem",
341 | 7: "1.75rem",
342 | 8: "2rem",
343 | 9: "2.25rem",
344 | 10: "2.5rem",
345 | 11: "2.75rem",
346 | 12: "3rem",
347 | 14: "3.5rem",
348 | 16: "4rem",
349 | },
350 | boxShadowSpread: {
351 | px: "1px",
352 | 0: "0",
353 | 0.5: "0.125rem",
354 | 1: "0.25rem",
355 | 1.5: "0.375rem",
356 | 2: "0.5rem",
357 | 2.5: "0.625rem",
358 | 3: "0.75rem",
359 | 3.5: "0.875rem",
360 | 4: "1rem",
361 | },
362 | boxShadowOpacity: shadowOpacityDefaultTheme,
363 | boxShadowLayersScale: shadowScaleDefaultTheme,
364 | boxShadowLayers: {
365 | 2: "2",
366 | 3: "3",
367 | 4: "4",
368 | 5: "5",
369 | 6: "6",
370 | 7: "7",
371 | 8: "8",
372 | },
373 | },
374 | }
375 | );
376 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2021",
4 | "module": "CommonJS",
5 | "moduleResolution": "Node",
6 | "rootDir": "src",
7 | "outDir": "dist",
8 | "declaration": true,
9 | "declarationDir": "dist"
10 | },
11 | "include": ["src/**/*"],
12 | "exclude": ["src/experiment.ts"]
13 | }
14 |
--------------------------------------------------------------------------------