diff --git a/README.md b/README.md index 2a5386f..82d7484 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,61 @@ A framework agnostic skeleton loader that supports syncing multiple elements' lo ![A skeleton loader with a line going through multiple elements at the same time despite different widths](./media/skeleton.gif) +## 📐 CSS Variables for customising + +### 🎨 Styling / Design; + +| Variable | default | type |Description | +|----------|---------|------|-------------| +| `--skeleton-base-color` | `hsl(0 0% 92% / 100%)` | `` | The base color of the skeleton | +| `--skeleton-base-color-dark` | `hsl(0 0% 20% / 100%)` | `` | The base color of the skeleton in dark mode | +| `--skeleton-highlight-color` | `hsl(0 0% 96% / 100%)` | `` | The highlight color of the skeleton | +| `--skeleton-highlight-color-dark` | `hsl(0 0% 15% / 100%)` | `` | The highlight color of the skeleton in dark mode | +| `--skeleton-highlight-size` | `clamp(100px, 50vw, 500px)` | `` | The width of the highlight wave gradient | +| `--skeleton-highlight-leading-edge-blur` | `clamp(10px, 5vw, 50px)` | `` | How much to blur the leading edge of the highlight | +| `--skeleton-radius` | `0px` | `` | The radius of the skeleton | +| `--skeleton-angle` | `90deg` | `` | The angle of the highlight | + +### 🎬 Animation; + +| Variable | default | type | Description | +|----------|---------|------|-------------| +| `--skeleton-repeat` | `infinite` | `` | How many times the animation will repeat | +| `--skeleton-ease` | `cubic-bezier(0.45, 0.3, 0.7, 0.55)` | `` | The ease of the animation | +| `--skeleton-animation-animation-speed` | `400` | `` | _(px)_ - The distance the animation will travel in pixels | +| `--skeleton-animation-animation-time` | `1` | `` | _(seconds)_ - The time of each animation loop | +| `--skeleton-animation-animation-scaler` | `0.7` | `` | _(0-1)_ - How much to scale the animation speed up as the screen size increases, 0.7 feels good. | + +### 📝 Example; + +```css +/* example of modifying the default color variables */ +:root { + --skeleton-base-color: orangered; + --skeleton-base-color-dark: tomato; + --skeleton-highlight-color: darkslateblue; + --skeleton-highlight-color-dark: indigo; +} +``` + +## 🌙 Dark Mode; + +We automatically apply dark mode to the skeleton when the `color-scheme` is set to `dark` or `light dark` and the `prefers-color-scheme` is set to `dark`, +which will then draw on the `--skeleton-base-color-dark` and `--skeleton-highlight-color-dark` variables. + +Additionally, you can add the `dark` class to the skeleton to force dark mode. + +```html +
+ ... +
+``` + +Or if your entire application is a dark theme, then you can simply change +the `--skeleton-base-color` to an appropriate shade of your choice. + +## 🔌 Adapters + We support adapters for: - [React](./packages/react) diff --git a/packages/core/skeleton.css b/packages/core/skeleton.css index dd196da..c192303 100644 --- a/packages/core/skeleton.css +++ b/packages/core/skeleton.css @@ -1,67 +1,176 @@ :root { - --skeleton-base-color: #ebebeb; - --skeleton-highlight-color: #f5f5f5; - --skeleton-highlight-size: 40px; - --skeleton-pseudo-element-display: block; - --skeleton-left: 0px; - --skeleton-percentage: 0; - /* Pixels per second */ - --skeleton-animation-animation-speed: 400px; -} + /* global skeleton vars */ -.loading-skeleton { - background-color: var(--skeleton-base-color); - position: relative; - overflow: hidden; -} + /* ↓ style */ -.loading-skeleton::after { - content: ' '; - display: var(--skeleton-pseudo-element-display); - position: absolute; - top: 0; - left: calc(0px - var(--skeleton-highlight-size)); - width: var(--skeleton-highlight-size); - height: 100%; - background-repeat: no-repeat; - background-image: linear-gradient( - 90deg, - transparent 0%, - var(--skeleton-highlight-color) 50%, - transparent 100% + --skeleton-base-color: hsl(0 0% 92% / 100%); + --skeleton-base-color-dark: hsl(0 0% 20% / 100%); + + --skeleton-highlight-color: hsl(0 0% 96% / 100%); + --skeleton-highlight-color-dark: hsl(0 0% 15% / 100%); + + /* global radius of skeletons */ + --skeleton-radius: 0px; + + /* highlight gradient */ + --skeleton-highlight-size: clamp( + 100px, + 50vw, + 500px + ); /* size of the gradient that moves */ + --skeleton-highlight-leading-edge-blur: clamp( + 10px, + 5vw, + 50px + ); /* set to 0px for a sharp edge */ + --skeleton-angle: 90deg; /* gradient's angle */ + + /* ↓ effect */ + + /* time to travel a given distance */ + --skeleton-animation-animation-speed: 400; /* travel this many PIXELS */ + --skeleton-animation-animation-time: 1; /* over this many SECONDS */ + --skeleton-animation-animation-scaler: 0.7; /* scale the speed up as the screen size increases (0-1), 0.7 feels good. */ + + /* animation */ + --skeleton-repeat: infinite; /* highly suggest to not repeat this infinitely, for performance, 3 or 5 is pretty good. */ + --skeleton-ease: cubic-bezier(0.45, 0.3, 0.7, 0.55); + + /* ↓ stuff to calc, don't edit */ + + --skeleton-base: light-dark( + var(--skeleton-base-color), + var(--skeleton-base-color-dark) + ); + --skeleton-highlight: light-dark( + var(--skeleton-highlight-color), + var(--skeleton-highlight-color-dark) + ); + + --skeleton-percentage: 0; /* the value we will animate to make the gradient move */ + /* interesting hack to get viewport as an Integer; + https://dev.to/janeori/css-type-casting-to-numeric-tanatan2-scalars-582j */ + + --skeleton-full-viewport: 100vw; + --skeleton-calc-int-width: calc( + 10000 * tan(atan2(var(--skeleton-full-viewport), 10000px)) ); - transform: translateX( - calc( - (100vw + var(--skeleton-highlight-size)) * - calc(var(--skeleton-percentage) / 100) - var(--skeleton-left) + /* scale the animation speed so it gets faster as the screen gets larger, + because elements don't normally take up 100% width on larger screens + and so the animation would slow down as screen gets larger without this. */ + --skeleton-calc-scaled-time: calc( + ( + 1 - (var(--skeleton-calc-int-width) / 400) * + var(--skeleton-animation-animation-scaler) / 5 ) ); - --skeleton-second: 1000ms; - --skeleton-distance: calc(100vw + var(--skeleton-highlight-size)); - --skeleton-animation-duration: calc( - calc(var(--skeleton-distance) / var(--skeleton-animation-animation-speed)) * - var(--skeleton-second) + /* get the animation time as a fraction of our desired distance/time */ + --skeleton-calc-anim-time: calc( + ( + ( + var(--skeleton-calc-int-width) / + var(--skeleton-animation-animation-speed) + ) * + (var(--skeleton-animation-animation-time) * 1.33) + ) * + var(--skeleton-calc-scaled-time) ); - animation: var(--skeleton-animation-duration) skeletonPercent infinite; + /* we have the animation waiting for 1/3 of the time before starting each loop, + so we multiple total time by 1.33 to account for it */ } -@media (prefers-reduced-motion) { +.loading-skeleton { + + color: transparent; + user-select: none; + background-color: var(--skeleton-base); + border-radius: var(--skeleton-radius); + + --anim-pos: calc( + var(--skeleton-percentage) * (100vw + var(--skeleton-highlight-size) * 2) - + var(--skeleton-highlight-size) + ); + + /* eased gradient, + creates a smoother gradient than a simple 'to right, transparent, white' */ + + background-image: linear-gradient( + var(--skeleton-angle, 90deg), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 0 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.013)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 11.8 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.049)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 22.4 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.104)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 32 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.175)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 40.6 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.259)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 48.4 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.352)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 55.4 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.45)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 61.7 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.55)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 67.4 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.648)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 72.7 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.741)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 77.6 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.825)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 82.3 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.896)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 86.7 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.951)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 91.1 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0.987)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 95.5 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 1)) + calc(var(--anim-pos) + (var(--skeleton-highlight-size) * 100 / 100)), + hsl(from var(--skeleton-highlight) h s l / calc(alpha * 0)) + calc( + var(--anim-pos) + + ( + var(--skeleton-highlight-size) + + var(--skeleton-highlight-leading-edge-blur) + ) + ) + ); + + background-attachment: fixed; +} + +@media not (prefers-reduced-motion) { .loading-skeleton { - --skeleton-pseudo-element-display: none; + animation: skeletonPercent calc(1000ms * var(--skeleton-calc-anim-time)) + var(--skeleton-ease) var(--skeleton-repeat); } } +.loading-skeleton.dark { + color-scheme: dark; +} + @property --skeleton-percentage { syntax: ''; inherits: false; initial-value: 0; } +@property --skeleton-full-viewport { + syntax: ''; + initial-value: 0px; + inherits: false; +} + @keyframes skeletonPercent { 0% { --skeleton-percentage: 0; } + 66%, 100% { - --skeleton-percentage: 100; + --skeleton-percentage: 1; } + /* 66% means we wait for ~1/3 of the time after playing the animation for ~2/3 of the time */ }