text-reveal.css 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /*
  2. * TextReveal — mask-position wipe animation
  3. *
  4. * Instead of sliding text through a fixed mask (odometer style),
  5. * the mask itself sweeps across each span to reveal/hide text.
  6. *
  7. * Direction: top-to-bottom. New text drops in from above, old text exits downward.
  8. *
  9. * Entering: gradient reveals top-to-bottom (top of text appears first).
  10. * gradient(to bottom, white 33%, transparent 33%+edge)
  11. * pos 0 100% = transparent covers element = hidden
  12. * pos 0 0% = white covers element = visible
  13. *
  14. * Leaving: gradient hides top-to-bottom (top of text disappears first).
  15. * gradient(to top, white 33%, transparent 33%+edge)
  16. * pos 0 100% = white covers element = visible
  17. * pos 0 0% = transparent covers element = hidden
  18. *
  19. * Both transition from 0 100% (swap) → 0 0% (settled).
  20. */
  21. [data-component="text-reveal"] {
  22. --_edge: var(--text-reveal-edge, 17%);
  23. --_dur: var(--text-reveal-duration, 450ms);
  24. --_spring: var(--text-reveal-spring, cubic-bezier(0.34, 1.08, 0.64, 1));
  25. --_spring-soft: var(--text-reveal-spring-soft, cubic-bezier(0.34, 1, 0.64, 1));
  26. --_travel: var(--text-reveal-travel, 0px);
  27. display: inline-flex;
  28. align-items: center;
  29. min-width: 0;
  30. overflow: visible;
  31. [data-slot="text-reveal-track"] {
  32. display: grid;
  33. min-height: 20px;
  34. line-height: 20px;
  35. justify-items: start;
  36. align-items: center;
  37. overflow: visible;
  38. transition: width var(--_dur) var(--_spring-soft);
  39. }
  40. [data-slot="text-reveal-entering"],
  41. [data-slot="text-reveal-leaving"] {
  42. grid-area: 1 / 1;
  43. line-height: 20px;
  44. white-space: nowrap;
  45. justify-self: start;
  46. text-align: start;
  47. mask-size: 100% 300%;
  48. -webkit-mask-size: 100% 300%;
  49. mask-repeat: no-repeat;
  50. -webkit-mask-repeat: no-repeat;
  51. transition-duration: var(--_dur);
  52. transition-timing-function: var(--_spring);
  53. }
  54. /* ── entering: reveal top-to-bottom ──
  55. * Gradient(to top): white at bottom, transparent at top of mask.
  56. * Settled pos 0 100% = white covers element = visible
  57. * Swap pos 0 0% = transparent covers = hidden
  58. * Slides from above: translateY(-travel) → translateY(0)
  59. */
  60. [data-slot="text-reveal-entering"] {
  61. mask-image: linear-gradient(to top, white 33%, transparent calc(33% + var(--_edge)));
  62. -webkit-mask-image: linear-gradient(to top, white 33%, transparent calc(33% + var(--_edge)));
  63. mask-position: 0 100%;
  64. -webkit-mask-position: 0 100%;
  65. transition-property:
  66. mask-position,
  67. -webkit-mask-position,
  68. transform;
  69. transform: translateY(0);
  70. }
  71. /* ── leaving: hide top-to-bottom + slide downward ──
  72. * Gradient(to bottom): white at top, transparent at bottom of mask.
  73. * Swap pos 0 0% = white covers element = visible
  74. * Settled pos 0 100% = transparent covers = hidden
  75. * Slides down: translateY(0) → translateY(travel)
  76. */
  77. [data-slot="text-reveal-leaving"] {
  78. mask-image: linear-gradient(to bottom, white 33%, transparent calc(33% + var(--_edge)));
  79. -webkit-mask-image: linear-gradient(to bottom, white 33%, transparent calc(33% + var(--_edge)));
  80. mask-position: 0 100%;
  81. -webkit-mask-position: 0 100%;
  82. transition-property:
  83. mask-position,
  84. -webkit-mask-position,
  85. transform;
  86. transform: translateY(var(--_travel));
  87. }
  88. /* ── swapping: instant reset ──
  89. * Snap entering to hidden (above), leaving to visible (center).
  90. */
  91. &[data-swapping="true"] [data-slot="text-reveal-entering"] {
  92. mask-position: 0 0%;
  93. -webkit-mask-position: 0 0%;
  94. transform: translateY(calc(var(--_travel) * -1));
  95. transition-duration: 0ms !important;
  96. }
  97. &[data-swapping="true"] [data-slot="text-reveal-leaving"] {
  98. mask-position: 0 0%;
  99. -webkit-mask-position: 0 0%;
  100. transform: translateY(0);
  101. transition-duration: 0ms !important;
  102. }
  103. /* ── not ready: kill all transitions ── */
  104. &[data-ready="false"] [data-slot="text-reveal-track"] {
  105. transition-duration: 0ms !important;
  106. }
  107. &[data-ready="false"] [data-slot="text-reveal-entering"],
  108. &[data-ready="false"] [data-slot="text-reveal-leaving"] {
  109. transition-duration: 0ms !important;
  110. }
  111. &[data-truncate="true"] {
  112. width: 100%;
  113. }
  114. &[data-truncate="true"] [data-slot="text-reveal-track"] {
  115. width: 100%;
  116. min-width: 0;
  117. overflow: hidden;
  118. }
  119. &[data-truncate="true"] [data-slot="text-reveal-entering"],
  120. &[data-truncate="true"] [data-slot="text-reveal-leaving"] {
  121. min-width: 0;
  122. width: 100%;
  123. overflow: hidden;
  124. text-overflow: ellipsis;
  125. }
  126. }
  127. @media (prefers-reduced-motion: reduce) {
  128. [data-component="text-reveal"] [data-slot="text-reveal-track"] {
  129. transition-duration: 0ms !important;
  130. }
  131. [data-component="text-reveal"] [data-slot="text-reveal-entering"],
  132. [data-component="text-reveal"] [data-slot="text-reveal-leaving"] {
  133. transition-duration: 0ms !important;
  134. }
  135. }