├── .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 | --------------------------------------------------------------------------------