tailwind.smooth-shadow.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. /**
  2. * Smooth shadows for Tailwind CSS 3.
  3. *
  4. * A port of the `shadow-plugin` package by Florian Kiem (MIT, https://shadow.floriankiem.com),
  5. * whose published build targets Tailwind 4 only (`@theme` / `@utility`). Class names and shadow
  6. * values are identical, so switching to the npm package once UNA moves to Tailwind 4 needs no
  7. * template changes.
  8. *
  9. * - `smooth-shadow-{xs|sm|md|lg|xl|2xl}` — stacked, eased shadow layers (`smooth-shadow` = md).
  10. * - `smooth-shadow-ring-{size}` — the same stack with a 1px hairline ring baked in as the last
  11. * layer, for elevated surfaces (blocks, cards, popups, menus). Never pair it with `border`/`ring`.
  12. * - `shadow-{color}` tints the shadow, `smooth-ring-{color}` tints the ring; both compose.
  13. * - The ring is black 5% in light mode and white 18% under `.dark` / `[data-theme="dark"]`.
  14. * - `smooth-shadow-border` — a shadow in place of a border for surfaces that only need an edge (the
  15. * header bar, page blocks): a 1px ring, a 1px lift and a 2px ambient layer in light mode, and a single
  16. * white ring under `.dark`, where depth shadows don't show. Never pair it with `border`/`ring`.
  17. * - `smooth-shadow-popover` — the same edge for floating surfaces (popups, dropdown menus), plus a
  18. * soft drop shadow that lifts them off the page in both modes.
  19. */
  20. const plugin = require('tailwindcss/plugin');
  21. const flattenColorPalette = require('tailwindcss/lib/util/flattenColorPalette').default;
  22. // [offset-y, blur, spread, alpha %]
  23. const LAYERS = {
  24. xs: [[0, 4, 0, 4]],
  25. sm: [[18, 47, 0, 3], [7.5, 19, 0, 2], [4, 10.5, 0, 2], [2.3, 5.8, 0, 1], [1.2, 3.1, 0, 1], [0.5, 1.3, 0, 1]],
  26. md: [[17.54, 23.39, 0, 4], [9.4, 12.5, 0, 3], [5.25, 7, 0, 2], [2.79, 3.72, -2, 1], [1.16, 1.5, 0, 1]],
  27. lg: [[25, 50, 0, 5], [12, 24, 0, 4], [6, 12, 0, 3], [3, 6, 0, 2], [1.5, 3, 0, 2]],
  28. xl: [[40, 80, 0, 6], [20, 40, 0, 5], [10, 20, 0, 4], [5, 10, 0, 3], [2, 4, 0, 2]],
  29. '2xl': [[60, 120, 0, 7], [30, 60, 0, 6], [15, 30, 0, 5], [7.5, 15, 0, 4], [3, 6, 0, 3]],
  30. };
  31. const RING = '0 0 0 var(--smooth-ring-width, 1px) var(--smooth-ring-color)';
  32. const BORDER_LIGHT = '0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px -1px oklch(0 0 0 / 0.06), 0 2px 4px 0 oklch(0 0 0 / 0.04)';
  33. const BORDER_DARK = '0 0 0 1px oklch(1 0 0 / 0.08)';
  34. const POPOVER_LIGHT = '0 0 0 1px oklch(0 0 0 / 0.06), 0 4px 6px -2px oklch(0 0 0 / 0.05), 0 12px 24px -4px oklch(0 0 0 / 0.10)';
  35. const POPOVER_DARK = '0 0 0 1px oklch(1 0 0 / 0.10), 0 12px 24px -4px oklch(0 0 0 / 0.50)';
  36. function stack(size, withRing) {
  37. const layers = LAYERS[size].map(([y, blur, spread, alpha]) =>
  38. `0 ${y}px ${blur}px ${spread}px color-mix(in srgb, var(--smooth-shadow-color) ${alpha}%, transparent)`
  39. );
  40. if (withRing)
  41. layers.push(RING);
  42. return layers.join(', ');
  43. }
  44. module.exports = plugin(function ({ addBase, addUtilities, matchUtilities, theme }) {
  45. addBase({
  46. ':root': {
  47. '--smooth-ring-color': 'rgba(0, 0, 0, 0.05)',
  48. '--smooth-ring-width': '1px',
  49. '--smooth-shadow-border': BORDER_LIGHT,
  50. '--smooth-shadow-popover': POPOVER_LIGHT,
  51. },
  52. '.light, [data-theme="light"]': {
  53. '--smooth-ring-color': 'rgba(0, 0, 0, 0.05)',
  54. '--smooth-shadow-border': BORDER_LIGHT,
  55. '--smooth-shadow-popover': POPOVER_LIGHT,
  56. },
  57. '.dark, [data-theme="dark"]': {
  58. '--smooth-ring-color': 'rgba(255, 255, 255, 0.18)',
  59. '--smooth-shadow-border': BORDER_DARK,
  60. '--smooth-shadow-popover': POPOVER_DARK,
  61. },
  62. });
  63. const utilities = {
  64. '.smooth-shadow-none': { boxShadow: 'none' },
  65. };
  66. /**
  67. * Like Tailwind's own `shadow-*`, the stack is written to `--tw-shadow` and box-shadow is composed from
  68. * the ring variables, so `ring-*` utilities on the same element add to the shadow instead of replacing it.
  69. */
  70. const BOX_SHADOW = 'var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)';
  71. for (const size of Object.keys(LAYERS)) {
  72. for (const [name, withRing] of [[`smooth-shadow-${size}`, false], [`smooth-shadow-ring-${size}`, true]]) {
  73. const value = stack(size, withRing);
  74. utilities[`.${name}`] = {
  75. '--smooth-shadow-color': 'var(--tw-shadow-color, black)',
  76. '--tw-shadow': value,
  77. '--tw-shadow-colored': value,
  78. boxShadow: BOX_SHADOW,
  79. };
  80. }
  81. }
  82. utilities['.smooth-shadow'] = utilities['.smooth-shadow-md'];
  83. utilities['.smooth-shadow-ring'] = utilities['.smooth-shadow-ring-md'];
  84. utilities['.smooth-shadow-border'] = {
  85. '--tw-shadow': 'var(--smooth-shadow-border)',
  86. '--tw-shadow-colored': 'var(--smooth-shadow-border)',
  87. boxShadow: BOX_SHADOW,
  88. };
  89. utilities['.smooth-shadow-popover'] = {
  90. '--tw-shadow': 'var(--smooth-shadow-popover)',
  91. '--tw-shadow-colored': 'var(--smooth-shadow-popover)',
  92. boxShadow: BOX_SHADOW,
  93. };
  94. addUtilities(utilities);
  95. matchUtilities(
  96. { 'smooth-ring': (value) => ({ '--smooth-ring-color': value }) },
  97. { values: flattenColorPalette(theme('colors')), type: 'color' }
  98. );
  99. });