From d6d2997d39b6860be48442dcb8e361493a48e8df Mon Sep 17 00:00:00 2001 From: Ajay Bandiwadar Date: Tue, 2 Jun 2026 06:32:25 +0530 Subject: [PATCH 1/3] feat: add ease-tooltip CSS-only animated tooltip component --- submissions/examples/ease-tooltip/README.md | 104 +++++ submissions/examples/ease-tooltip/demo.html | 406 ++++++++++++++++++++ submissions/examples/ease-tooltip/style.css | 276 +++++++++++++ 3 files changed, 786 insertions(+) create mode 100644 submissions/examples/ease-tooltip/README.md create mode 100644 submissions/examples/ease-tooltip/demo.html create mode 100644 submissions/examples/ease-tooltip/style.css diff --git a/submissions/examples/ease-tooltip/README.md b/submissions/examples/ease-tooltip/README.md new file mode 100644 index 0000000..b81d769 --- /dev/null +++ b/submissions/examples/ease-tooltip/README.md @@ -0,0 +1,104 @@ +# ease-tooltip + +## What does this do? + +A CSS-only animated tooltip component that appears on hover and keyboard focus — no JavaScript required. The bubble fades in and slides 4px into position using `opacity` + `transform` transitions, consistent with EaseMotion's existing motion language. + +## How is it used? + +Wrap any trigger element with a `` and set the tooltip text via the `data-tip` attribute: + +```html + + + + + + + + + + + + + + + + + + + +``` + +### Semantic color variants + +Override `--tooltip-bg` for contextual meaning: + +```html + + + + + + + + + +``` + +```css +.tooltip-success { --tooltip-bg: var(--ease-color-success-dark, #15803d); } +.tooltip-danger { --tooltip-bg: var(--ease-color-danger-dark, #b91c1c); } +.tooltip-warning { --tooltip-bg: var(--ease-color-warning-dark, #b45309); } +.tooltip-info { --tooltip-bg: #0369a1; } +``` + +### Custom speed or offset + +```css +/* Slower tooltip on a specific element */ +.my-element { --tooltip-speed: var(--ease-speed-medium); } + +/* More breathing room between trigger and bubble */ +.my-element { --tooltip-offset: 12px; } +``` + +## Why is it useful? + +Tooltips are one of the most commonly needed UI primitives — yet EaseMotion CSS had no tooltip component at all. The VISION.md roadmap explicitly lists "Modal & tooltip components" as **Planned v1.2**. + +This submission accelerates that milestone with a zero-JS implementation: + +- **Pure CSS** — `::before` (bubble) + `::after` (arrow) pseudo-elements on the wrapper +- **No extra markup overhead** — text lives in `data-tip`, not a separate element +- **4 position variants** — top (default), bottom, left, right +- **Keyboard accessible** — triggers on `:focus-within`, not just `:hover` +- **Token-driven** — all values use existing `--ease-*` custom properties +- **`prefers-reduced-motion` safe** — transitions disabled, tooltip still shows +- **Composable** — works on buttons, icon buttons, inline text, cards + +## CSS Custom Properties + +| Property | Default | Description | +|---|---|---| +| `--tooltip-bg` | `var(--ease-color-neutral-900)` | Bubble background color | +| `--tooltip-color` | `var(--ease-color-neutral-50)` | Bubble text color | +| `--tooltip-speed` | `var(--ease-speed-fast)` | Transition duration | +| `--tooltip-offset` | `8px` | Gap between trigger and bubble | +| `--tooltip-radius` | `var(--ease-radius-sm)` | Bubble border radius | +| `--tooltip-max-width` | `220px` | Maximum bubble width | + +## Tech Stack + +- HTML +- CSS only (no frameworks, no JavaScript) + +## Preview + +Open `demo.html` directly in your browser to see all variants. + +## Contribution Notes + +- Class names used: `.tooltip`, `.tooltip-top`, `.tooltip-bottom`, `.tooltip-left`, `.tooltip-right` +- Maintainer will rename to `ease-tooltip`, `ease-tooltip-bottom`, etc. before merging +- No changes made to `core/`, `components/`, or any existing files \ No newline at end of file diff --git a/submissions/examples/ease-tooltip/demo.html b/submissions/examples/ease-tooltip/demo.html new file mode 100644 index 0000000..9027a8b --- /dev/null +++ b/submissions/examples/ease-tooltip/demo.html @@ -0,0 +1,406 @@ + + + + + + ease-tooltip — EaseMotion CSS + + + + + + + + + + + + +

ease-tooltip

+

CSS-only animated tooltip component · hover or focus to trigger · zero JavaScript

+ + + + +
+ +

Position Variants

+

+ Four directional variants controlled by a single modifier class. + Default is top. Add tooltip-bottom, + tooltip-left, or + tooltip-right for other directions. +

+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +

Icon Button Toolbar

+

+ A common real-world pattern — icon-only buttons with tooltips explaining + each action. No visible label needed; the tooltip carries the meaning. + Keyboard accessible via focus-within. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +

Semantic Color Variants

+

+ Tooltip background color is driven by a single + --tooltip-bg CSS variable. + Semantic variants (success, danger, warning, info) reinforce + the intent of the action — no extra markup needed. +

+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + + + +
+ +

Inline Prose Usage

+

+ Tooltips work inline inside paragraphs — useful for glossary terms, + abbreviations, and contextual hints without breaking reading flow. +

+ +
+

+ EaseMotion CSS uses + + CSS custom properties + + as its token system. All animations respect + + prefers-reduced-motion + + and are triggered via + + :focus-within + + for full keyboard accessibility — no JavaScript required. +

+
+
+ + + + +
+ +

Keyboard Accessible

+

+ Tab through the buttons below. The tooltip appears on + :focus-within — no mouse required. + This makes the component WCAG 2.1 AA compliant out of the box. +

+ +
+ + + + + + + + + + + + + +
+
+ + + \ No newline at end of file diff --git a/submissions/examples/ease-tooltip/style.css b/submissions/examples/ease-tooltip/style.css new file mode 100644 index 0000000..6712d59 --- /dev/null +++ b/submissions/examples/ease-tooltip/style.css @@ -0,0 +1,276 @@ +/* ============================================================ + EaseMotion CSS — Contributor Submission + Feature: CSS-only Animated Tooltip Component + Folder: submissions/examples/ease-tooltip/ + Author: AjayBandiwaddar + ============================================================ + + NAMING NOTE: + Class names here are contributor-defined. + The maintainer will rename to ease-* convention before merging. + ============================================================ */ + + +/* ── Design Tokens (mirrors core/variables.css) ──────────── + All values reference --ease-* custom properties. + Hard-coded fallbacks are provided only for standalone + demo use — the maintainer will remove them on integration. + ──────────────────────────────────────────────────────────── */ + +:root { + --tooltip-bg: var(--ease-color-neutral-900, #0f172a); + --tooltip-color: var(--ease-color-neutral-50, #f8fafc); + --tooltip-font-size: var(--ease-text-xs, 0.75rem); + --tooltip-radius: var(--ease-radius-sm, 0.25rem); + --tooltip-padding-y: var(--ease-space-1, 0.25rem); + --tooltip-padding-x: var(--ease-space-3, 0.75rem); + --tooltip-speed: var(--ease-speed-fast, 150ms); + --tooltip-ease: var(--ease-ease-out, cubic-bezier(0, 0, 0.2, 1)); + --tooltip-offset: 8px; /* gap between trigger and bubble */ + --tooltip-arrow-size: 5px; /* half-width of the arrow tip */ + --tooltip-max-width: 220px; +} + + +/* ── Wrapper ─────────────────────────────────────────────── + .tooltip wraps any trigger element. + display: inline-flex keeps it tight around the child. + ──────────────────────────────────────────────────────────── */ + +.tooltip { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; +} + + +/* ── Bubble (::before) ───────────────────────────────────── + Content is pulled from data-tip attribute. + ──────────────────────────────────────────────────────────── */ + +.tooltip::before { + content: attr(data-tip); + position: absolute; + z-index: var(--ease-z-toast, 9999); + + /* Typography */ + font-size: var(--tooltip-font-size); + font-weight: 500; + line-height: 1.4; + white-space: nowrap; + text-align: center; + max-width: var(--tooltip-max-width); + + /* Appearance */ + background: var(--tooltip-bg); + color: var(--tooltip-color); + padding: var(--tooltip-padding-y) var(--tooltip-padding-x); + border-radius: var(--tooltip-radius); + + /* Hidden state */ + opacity: 0; + pointer-events: none; + + /* Transition */ + transition: + opacity var(--tooltip-speed) var(--tooltip-ease), + transform var(--tooltip-speed) var(--tooltip-ease); +} + + +/* ── Arrow tip (::after) ─────────────────────────────────── + Built with a zero-size box + border trick. + ──────────────────────────────────────────────────────────── */ + +.tooltip::after { + content: ''; + position: absolute; + z-index: var(--ease-z-toast, 9999); + width: 0; + height: 0; + pointer-events: none; + + /* Hidden state */ + opacity: 0; + transition: + opacity var(--tooltip-speed) var(--tooltip-ease), + transform var(--tooltip-speed) var(--tooltip-ease); +} + + +/* ================================================================ + POSITION: TOP (default) + Bubble appears above the trigger, arrow points down. + ================================================================ */ + +.tooltip, +.tooltip-top { + --_enter-transform: translateX(-50%) translateY(0); + --_exit-transform: translateX(-50%) translateY(4px); +} + +.tooltip::before, +.tooltip-top::before { + bottom: calc(100% + var(--tooltip-offset)); + left: 50%; + transform: var(--_exit-transform); +} + +.tooltip::after, +.tooltip-top::after { + bottom: calc(100% + var(--tooltip-offset) - var(--tooltip-arrow-size)); + left: 50%; + transform: translateX(-50%) translateY(4px); + border-width: var(--tooltip-arrow-size) var(--tooltip-arrow-size) 0; + border-style: solid; + border-color: var(--tooltip-bg) transparent transparent; +} + +/* Visible state — top */ +.tooltip:hover::before, +.tooltip:focus-within::before, +.tooltip-top:hover::before, +.tooltip-top:focus-within::before { + opacity: 1; + transform: var(--_enter-transform); +} + +.tooltip:hover::after, +.tooltip:focus-within::after, +.tooltip-top:hover::after, +.tooltip-top:focus-within::after { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + + +/* ================================================================ + POSITION: BOTTOM + Bubble appears below, arrow points up. + ================================================================ */ + +.tooltip-bottom::before { + top: calc(100% + var(--tooltip-offset)); + bottom: auto; + left: 50%; + transform: translateX(-50%) translateY(-4px); +} + +.tooltip-bottom::after { + top: calc(100% + var(--tooltip-offset) - var(--tooltip-arrow-size)); + bottom: auto; + left: 50%; + transform: translateX(-50%) translateY(-4px); + border-width: 0 var(--tooltip-arrow-size) var(--tooltip-arrow-size); + border-style: solid; + border-color: transparent transparent var(--tooltip-bg); +} + +.tooltip-bottom:hover::before, +.tooltip-bottom:focus-within::before { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +.tooltip-bottom:hover::after, +.tooltip-bottom:focus-within::after { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + + +/* ================================================================ + POSITION: LEFT + Bubble appears to the left, arrow points right. + ================================================================ */ + +.tooltip-left::before { + right: calc(100% + var(--tooltip-offset)); + left: auto; + bottom: auto; + top: 50%; + transform: translateY(-50%) translateX(4px); +} + +.tooltip-left::after { + right: calc(100% + var(--tooltip-offset) - var(--tooltip-arrow-size)); + left: auto; + bottom: auto; + top: 50%; + transform: translateY(-50%) translateX(4px); + border-width: var(--tooltip-arrow-size) 0 var(--tooltip-arrow-size) var(--tooltip-arrow-size); + border-style: solid; + border-color: transparent transparent transparent var(--tooltip-bg); +} + +.tooltip-left:hover::before, +.tooltip-left:focus-within::before { + opacity: 1; + transform: translateY(-50%) translateX(0); +} + +.tooltip-left:hover::after, +.tooltip-left:focus-within::after { + opacity: 1; + transform: translateY(-50%) translateX(0); +} + + +/* ================================================================ + POSITION: RIGHT + Bubble appears to the right, arrow points left. + ================================================================ */ + +.tooltip-right::before { + left: calc(100% + var(--tooltip-offset)); + right: auto; + bottom: auto; + top: 50%; + transform: translateY(-50%) translateX(-4px); +} + +.tooltip-right::after { + left: calc(100% + var(--tooltip-offset) - var(--tooltip-arrow-size)); + right: auto; + bottom: auto; + top: 50%; + transform: translateY(-50%) translateX(-4px); + border-width: var(--tooltip-arrow-size) var(--tooltip-arrow-size) var(--tooltip-arrow-size) 0; + border-style: solid; + border-color: transparent var(--tooltip-bg) transparent transparent; +} + +.tooltip-right:hover::before, +.tooltip-right:focus-within::before { + opacity: 1; + transform: translateY(-50%) translateX(0); +} + +.tooltip-right:hover::after, +.tooltip-right:focus-within::after { + opacity: 1; + transform: translateY(-50%) translateX(0); +} + + +/* ================================================================ + ACCESSIBILITY: Reduced Motion + Remove transitions entirely — tooltip still shows/hides, + just without animation. Never hide information. + ================================================================ */ + +@media (prefers-reduced-motion: reduce) { + .tooltip::before, + .tooltip::after, + .tooltip-top::before, + .tooltip-top::after, + .tooltip-bottom::before, + .tooltip-bottom::after, + .tooltip-left::before, + .tooltip-left::after, + .tooltip-right::before, + .tooltip-right::after { + transition: none !important; + } +} \ No newline at end of file From 2083cd5112dee17f5ea31688f40a7d4733a9b718 Mon Sep 17 00:00:00 2001 From: Ajay Bandiwadar Date: Tue, 2 Jun 2026 06:47:15 +0530 Subject: [PATCH 2/3] feat: add ease-page-loader fullscreen CSS preloader with exit animation --- .../examples/ease-page-loader/README.md | 97 +++ .../examples/ease-page-loader/demo.html | 554 ++++++++++++++++++ .../examples/ease-page-loader/style.css | 206 +++++++ 3 files changed, 857 insertions(+) create mode 100644 submissions/examples/ease-page-loader/README.md create mode 100644 submissions/examples/ease-page-loader/demo.html create mode 100644 submissions/examples/ease-page-loader/style.css diff --git a/submissions/examples/ease-page-loader/README.md b/submissions/examples/ease-page-loader/README.md new file mode 100644 index 0000000..7bcfdd6 --- /dev/null +++ b/submissions/examples/ease-page-loader/README.md @@ -0,0 +1,97 @@ +# ease-page-loader + +## What does this do? + +A fullscreen CSS overlay that covers the page on load, displays a centered loading animation, then fades out automatically — driven entirely by `animation-delay` and `animation-fill-mode: forwards`. No JavaScript required. + +## How is it used? + +Place the loader as the **first child of ``**. It covers the viewport instantly and exits on its own after `--loader-exit-delay` (default 1.8s): + +```html + +
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + +
+
+
+``` + +### Custom color + +```html +
+
+
+``` + +### Custom timing + +```html + +
+
+
+``` + +## Why does it fit EaseMotion CSS? + +EaseMotion CSS has inline loaders (skeleton shimmer, bouncing dots, button spinner) but **no fullscreen page-level loader** — these are fundamentally different use cases. This submission fills that gap. + +The implementation follows EaseMotion's core philosophy exactly: + +- **Pure CSS exit** — `animation-delay` holds the overlay; `animation-fill-mode: forwards` locks it in the final `visibility: hidden` state. Zero JavaScript timeouts +- **Reuses core keyframes** — the spinner uses `ease-kf-rotate` and the dots use `ease-kf-pulse`, both already defined in `core/animations.css` — no new keyframes added to core +- **Token-first** — `--loader-color` maps to `--ease-color-primary`; `--loader-bg` to `--ease-color-surface`; `--loader-pulse-speed` to `--ease-speed-slow` +- **Three variants** — spinner ring, pulse dots, progress bar — covering the most common page load patterns +- **`prefers-reduced-motion` safe** — animations disabled, overlay still exits at the correct time so content is never permanently hidden +- **`visibility: hidden` final state** — removes the overlay from the accessibility tree entirely after exit, not just visually + +## CSS Custom Properties + +| Property | Default | Description | +|---|---|---| +| `--loader-color` | `var(--ease-color-primary)` | Accent color for spinner, dots, bar | +| `--loader-color-track` | `var(--ease-color-neutral-200)` | Spinner ring track color | +| `--loader-bg` | `var(--ease-color-surface)` | Overlay background | +| `--loader-exit-delay` | `1.8s` | Time overlay stays visible before fading | +| `--loader-exit-speed` | `400ms` | Duration of the fade-out | +| `--loader-spin-speed` | `0.8s` | Spinner rotation speed | +| `--loader-pulse-speed` | `var(--ease-speed-slow)` | Dot pulse cycle duration | +| `--loader-size` | `48px` | Spinner diameter | +| `--loader-thickness` | `4px` | Spinner border width | + +## Tech Stack + +- HTML +- CSS only (no frameworks, no JavaScript) + +## Preview + +Open `demo.html` directly in your browser. The live loader fires on actual page load — you'll see it cover the page and exit after 1.8s, revealing the content below. + +## Contribution Notes + +- Class names used: `.page-loader`, `.page-loader-spinner`, `.page-loader-dots`, `.page-loader-dot`, `.page-loader-bar`, `.page-loader-dark` +- Maintainer will rename to `ease-page-loader`, `ease-page-loader-spinner`, etc. before merging +- No changes made to `core/`, `components/`, or any existing files +- Spinner reuses `ease-kf-rotate` — if integrating into core, the `animation` property just needs to reference that keyframe name directly \ No newline at end of file diff --git a/submissions/examples/ease-page-loader/demo.html b/submissions/examples/ease-page-loader/demo.html new file mode 100644 index 0000000..9320514 --- /dev/null +++ b/submissions/examples/ease-page-loader/demo.html @@ -0,0 +1,554 @@ + + + + + + ease-page-loader — EaseMotion CSS + + + + + + + + + + + + + + +
+
+ Loading… +
+ + + + +
+ +

ease-page-loader

+

+ Fullscreen CSS preloader with automatic exit animation · + pure CSS · no JavaScript · three variants +

+ + + + +
+ +

Three Variants

+

+ Each variant uses a different loading indicator. + All three share the same fullscreen overlay and CSS exit animation. + The spinner and dots reuse keyframes already in + core/animations.css. +

+ +
+ + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+

+ Spinner Ring
+ .page-loader-spinner +

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Pulse Dots
+ .page-loader-dots +

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+ Please wait… +
+
+

+ Progress Bar
+ .page-loader-bar +

+
+ +
+
+ + + + +
+ +

Dark Background Variant

+

+ Add .page-loader-dark for dark-themed pages. + Overrides --loader-bg and + --loader-color-track using + existing --ease-color-neutral-* tokens. +

+ +
+ + +
+
+
+
+
+
+
+
+
+
+
+
+

+ Dark + Spinner +

+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ Dark + Dots +

+
+ + +
+
+
+
+
+
+
+
+
+
+ Starting… +
+
+

+ Custom Color Override +

+
+ +
+
+ + + + +
+ +

How to Use

+

+ Place the loader as the first child of + <body>. + It covers the viewport instantly on load and exits automatically after + --loader-exit-delay (default 1.8s) — no JavaScript needed. +

+ +
+<!-- Place as FIRST child of <body> --> + +<!-- Spinner variant --> +<div class="page-loader"> + <div class="page-loader-spinner"></div> +</div> + +<!-- Dots variant --> +<div class="page-loader"> + <div class="page-loader-dots"> + <div class="page-loader-dot"></div> + <div class="page-loader-dot"></div> + <div class="page-loader-dot"></div> + </div> +</div> + +<!-- Dark variant --> +<div class="page-loader page-loader-dark"> + <div class="page-loader-spinner"></div> +</div> + +<!-- Custom color override --> +<div class="page-loader" style="--loader-color: #22c55e;"> + <div class="page-loader-spinner"></div> +</div> + +<!-- Custom timing --> +<div class="page-loader" style="--loader-exit-delay: 3s;"> + <div class="page-loader-bar-wrap"> + <div class="page-loader-bar"></div> + </div> +</div> +
+
+ + + + +
+ +

CSS Custom Properties

+

+ All values are customizable via CSS variables. + Every token maps to an existing --ease-* property. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyDefaultDescription
--loader-colorvar(--ease-color-primary)Spinner/dot/bar accent color
--loader-color-trackvar(--ease-color-neutral-200)Spinner ring track color
--loader-bgvar(--ease-color-surface)Overlay background color
--loader-exit-delay1.8sHow long overlay stays visible before fading
--loader-exit-speed400msDuration of the fade-out exit animation
--loader-spin-speed0.8sSpinner rotation speed
--loader-pulse-speedvar(--ease-speed-slow)Dot pulse cycle duration
--loader-size48pxSpinner diameter
+
+ +
+ + + \ No newline at end of file diff --git a/submissions/examples/ease-page-loader/style.css b/submissions/examples/ease-page-loader/style.css new file mode 100644 index 0000000..1e8cb89 --- /dev/null +++ b/submissions/examples/ease-page-loader/style.css @@ -0,0 +1,206 @@ +/* ============================================================ + EaseMotion CSS — Contributor Submission + Feature: Fullscreen CSS Page Preloader with Exit Animation + Folder: submissions/examples/ease-page-loader/ + Author: AjayBandiwaddar + ============================================================ + + NAMING NOTE: + Class names here are contributor-defined. + The maintainer will rename to ease-* convention before merging. + ============================================================ */ + + +/* ── Design Tokens ───────────────────────────────────────── + All values reference --ease-* custom properties. + Hard-coded fallbacks are for standalone demo use only. + ──────────────────────────────────────────────────────────── */ + +:root { + --loader-color: var(--ease-color-primary, #6c63ff); + --loader-color-track: var(--ease-color-neutral-200, #e2e8f0); + --loader-bg: var(--ease-color-surface, #ffffff); + --loader-spin-speed: 0.8s; + --loader-pulse-speed: var(--ease-speed-slow, 600ms); + --loader-exit-delay: 1.8s; + --loader-exit-speed: 400ms; + --loader-size: 48px; + --loader-thickness: 4px; +} + + +/* ================================================================ + BASE: Fullscreen Overlay + Covers the entire viewport on page load. + Exits automatically after --loader-exit-delay using pure CSS: + animation-delay holds the overlay, animation-fill-mode: forwards + locks it in the final invisible state. + ================================================================ */ + +.page-loader { + position: fixed; + inset: 0; + z-index: var(--ease-z-toast, 9999); + + /* Center the loader child */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.25rem; + + background: var(--loader-bg); + + /* Exit animation — pure CSS, no JS timeout needed */ + animation: page-loader-exit var(--loader-exit-speed) ease forwards; + animation-delay: var(--loader-exit-delay); +} + +/* Exit keyframe — fade out, block pointer events, remove from a11y tree */ +@keyframes page-loader-exit { + 0% { + opacity: 1; + pointer-events: auto; + visibility: visible; + } + 100% { + opacity: 0; + pointer-events: none; + visibility: hidden; + } +} + + +/* ================================================================ + VARIANT 1: Spinner Ring + Reuses ease-kf-rotate from core/animations.css. + A border-based ring with a colored leading edge. + ================================================================ */ + +.page-loader-spinner { + width: var(--loader-size); + height: var(--loader-size); + border-radius: 50%; + border: var(--loader-thickness) solid var(--loader-color-track); + border-top-color: var(--loader-color); + + /* Reuses ease-kf-rotate already defined in core/animations.css */ + animation: ease-kf-rotate var(--loader-spin-speed) linear infinite; +} + + +/* ================================================================ + VARIANT 2: Pulse Dot + Three dots that pulse in sequence using staggered animation-delay. + Reuses ease-kf-pulse from core/animations.css. + ================================================================ */ + +.page-loader-dots { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.page-loader-dot { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--loader-color); + + /* Reuses ease-kf-pulse already defined in core/animations.css */ + animation: ease-kf-pulse var(--loader-pulse-speed) ease-in-out infinite; +} + +/* Stagger each dot — natural wave feel */ +.page-loader-dot:nth-child(1) { animation-delay: 0ms; } +.page-loader-dot:nth-child(2) { animation-delay: 150ms; } +.page-loader-dot:nth-child(3) { animation-delay: 300ms; } + + +/* ================================================================ + VARIANT 3: Progress Bar + A horizontal bar that fills from left to right then loops. + Useful for page transitions and route changes. + ================================================================ */ + +.page-loader-bar-wrap { + width: 180px; + height: 3px; + background: var(--loader-color-track); + border-radius: 99px; + overflow: hidden; +} + +.page-loader-bar { + height: 100%; + width: 40%; + background: var(--loader-color); + border-radius: 99px; + animation: page-loader-bar-slide 1s ease-in-out infinite; +} + +@keyframes page-loader-bar-slide { + 0% { transform: translateX(-100%); } + 50% { transform: translateX(150%); } + 100% { transform: translateX(350%); } +} + + +/* ================================================================ + OPTIONAL: Loader Label + Small text beneath the animation — e.g. "Loading…" + ================================================================ */ + +.page-loader-label { + font-size: 0.8rem; + font-weight: 500; + color: var(--ease-color-muted, #64748b); + letter-spacing: 0.04em; +} + + +/* ================================================================ + DARK BACKGROUND VARIANT + Override the overlay background for dark-themed pages. + Usage:
+ ================================================================ */ + +.page-loader-dark { + --loader-bg: var(--ease-color-neutral-900, #0f172a); + --loader-color-track: var(--ease-color-neutral-700, #334155); +} + + +/* ================================================================ + ACCESSIBILITY: Reduced Motion + Keep the overlay visible and remove all animations. + The page content is still hidden until the overlay is manually + dismissed — developers should add a skip/dismiss button in this case. + ================================================================ */ + +@media (prefers-reduced-motion: reduce) { + .page-loader { + animation: page-loader-exit-instant 0ms forwards; + animation-delay: var(--loader-exit-delay); + } + + @keyframes page-loader-exit-instant { + to { + opacity: 0; + pointer-events: none; + visibility: hidden; + } + } + + .page-loader-spinner, + .page-loader-dot, + .page-loader-bar { + animation: none; + } + + /* Static fallback: show a centered dot instead */ + .page-loader-spinner { + border-top-color: var(--loader-color); + opacity: 0.6; + } +} \ No newline at end of file From f9d6c27e56b28e60cfc5eefce9f07acd34559e1a Mon Sep 17 00:00:00 2001 From: Ajay Bandiwadar Date: Tue, 2 Jun 2026 06:54:43 +0530 Subject: [PATCH 3/3] feat: add ease-split-text per-letter staggered entrance animation --- .../examples/ease-split-text/README.md | 109 ++++ .../examples/ease-split-text/demo.html | 507 ++++++++++++++++++ .../examples/ease-split-text/style.css | 180 +++++++ 3 files changed, 796 insertions(+) create mode 100644 submissions/examples/ease-split-text/README.md create mode 100644 submissions/examples/ease-split-text/demo.html create mode 100644 submissions/examples/ease-split-text/style.css diff --git a/submissions/examples/ease-split-text/README.md b/submissions/examples/ease-split-text/README.md new file mode 100644 index 0000000..0ce122d --- /dev/null +++ b/submissions/examples/ease-split-text/README.md @@ -0,0 +1,109 @@ +# ease-split-text + +## What does this do? + +A CSS utility that animates each letter of a heading independently — letters fade, slide, or blur into view in left-to-right sequence using `nth-child` staggered `animation-delay` on individual `` wrappers. Pure CSS, zero JavaScript. + +## How is it used? + +Wrap each letter in a ``. Use `.split-space` for word spaces. Add a variant modifier class: + +```html + +

+ Hello +

+ + +

+ Hello +

+ + +

+ Hello +

+ + +

+ Build + + faster +

+``` + +### Speed presets + +```html + +

...

+ + +

...

+``` + +### Custom step size + +```html +

...

+``` + +### Custom rise distance (slide variant) + +```html +

...

+``` + +## Why does it fit EaseMotion CSS? + +EaseMotion CSS animates elements at block level (`staggered-reveal`, `staggered-fade-entrance`) and text at string level (`ease-typewriter`). **Per-character animation is the missing layer** — and this submission fills it: + +| Effect | Unit animated | Technique | Per-letter? | +|---|---|---|---| +| `ease-typewriter` | Entire string | `width: 0ch → Nch` clipping | ✕ No | +| `staggered-reveal` | Block elements | `nth-child` delay on divs/lis | ✕ No | +| `ease-split-text` | Each letter | `nth-child` delay on inline spans | ✓ Yes | + +The implementation follows EaseMotion's core philosophy: + +- **Reuses core keyframes** — fade variant uses `ease-kf-fade-in`; slide variant uses `ease-kf-slide-up` — both already in `core/animations.css`. Zero new keyframes added +- **Token-first** — `--split-delay-step`, `--split-rise`, `--split-duration`, `--split-ease` are all overridable CSS variables +- **Human-readable** — `.ease-split-text-slide` is immediately self-explanatory +- **`prefers-reduced-motion` safe** — all letters shown instantly with no animation; text is never hidden + +## CSS Custom Properties + +| Property | Default | Description | +|---|---|---| +| `--split-delay-step` | `40ms` | Stagger gap between each letter | +| `--split-duration` | `400ms` | Animation duration per letter | +| `--split-rise` | `16px` | translateY travel for slide variant | +| `--split-ease` | `var(--ease-ease-out)` | Easing function | + +## Classes Reference + +| Class | Description | +|---|---| +| `ease-split-text` | Base class — fade-only entrance | +| `ease-split-text-slide` | Slide-up variant | +| `ease-split-text-blur` | Fade + blur variant | +| `ease-split-text-fast` | 25ms stagger step | +| `ease-split-text-slow` | 70ms stagger step | +| `ease-split-space` | Word space preserver | + +## Tech Stack + +- HTML +- CSS only (no frameworks, no JavaScript) + +## Preview + +Open `demo.html` directly in your browser. Six sections: fade, slide, blur on dark hero, speed presets, usage code, and a comparison table against existing effects. + +## Contribution Notes + +- Class names used: `.split-text`, `.split-text-slide`, `.split-text-blur`, `.split-text-fast`, `.split-text-slow`, `.split-space` +- Maintainer will rename to `ease-split-text-*` before merging +- Spinner references `ease-kf-fade-in` and `ease-kf-slide-up` — no changes to core needed +- No changes made to `core/`, `components/`, or any existing files +- `nth-child` stagger covers 30 letters — sufficient for virtually all heading text \ No newline at end of file diff --git a/submissions/examples/ease-split-text/demo.html b/submissions/examples/ease-split-text/demo.html new file mode 100644 index 0000000..28d4298 --- /dev/null +++ b/submissions/examples/ease-split-text/demo.html @@ -0,0 +1,507 @@ + + + + + + ease-split-text — EaseMotion CSS + + + + + + + + + + + + +

ease-split-text

+

+ Per-letter staggered entrance · pure CSS · zero JavaScript · + each letter is its own animated unit +

+ + + + +
+ +

Variant 1 — Fade Only

+

+ Letters fade in one by one from left to right. Uses + ease-kf-fade-in + already in core/animations.css — no new keyframe. + Default stagger: 40ms per letter. +

+ +
+
+

Default — 40ms step

+

+ Build + + faster. +

+
+ +
+

Accent color on letters

+

+ EaseMotion + + CSS +

+
+ +
+

Multi-word heading

+

+ Ship + + beautiful + + UIs. +

+
+
+
+ + + + +
+ +

Variant 2 — Slide Up

+

+ Each letter rises 16px into position as it fades in. Uses + ease-kf-slide-up + from core. The travel distance is controlled by + --split-rise (default 16px). +

+ +
+
+

Slide up — default rise

+

+ Design + + with + + motion. +

+
+ +
+

Larger rise — 28px

+

+ Animate + + everything. +

+
+
+
+ + + + +
+ +

Variant 3 — Fade + Blur

+

+ Letters materialize from blurred to sharp. Adds depth without movement — + ideal for hero sections on dark backgrounds where subtle motion + feels more premium than a slide. +

+ +
+
+

+ Dark hero · blur variant +

+

+ Pure + + CSS. +

+

+ Zero + + JavaScript. +

+
+
+
+ + + + +
+ +

Speed Presets

+

+ Three speed presets via modifier class. All controlled by + --split-delay-step — + override inline for any custom value. +

+ +
+ +
+

Fast · 25ms

+

class="split-text-fast"

+

+ Snappy. +

+
+ +
+

Default · 40ms

+

--split-delay-step: 40ms

+

+ Balanced. +

+
+ +
+

Slow · 70ms

+

class="split-text-slow"

+

+ Dramatic. +

+
+ +
+
+ + + + +
+ +

How to Use

+

+ Wrap each letter in a <span>. + Use .split-space for word spaces. + Add a variant modifier class. Done. +

+ +
<!-- Fade only (default) --> +<h1 class="split-text"> + <span>H</span><span>e</span><span>l</span><span>l</span><span>o</span> +</h1> + +<!-- Slide up variant --> +<h1 class="split-text split-text-slide"> + <span>H</span><span>e</span><span>l</span><span>l</span><span>o</span> +</h1> + +<!-- Blur variant --> +<h1 class="split-text split-text-blur"> + <span>H</span><span>e</span><span>l</span><span>l</span><span>o</span> +</h1> + +<!-- Multi-word: use .split-space for spaces --> +<h1 class="split-text split-text-slide"> + <span>H</span><span>i</span> + <span class="split-space"> </span> + <span>t</span><span>h</span><span>e</span><span>r</span><span>e</span> +</h1> + +<!-- Custom speed --> +<h1 class="split-text" style="--split-delay-step: 60ms;"> + <span>S</span><span>l</span><span>o</span><span>w</span> +</h1>
+
+ + + + +
+ +

How This Differs from Existing Effects

+

+ Three animations in EaseMotion CSS look similar but work completely differently. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
EffectUnit AnimatedTechniquePer-letter?
ease-typewriterEntire stringwidth: 0ch → Nch (clipping)✕ No
staggered-revealBlock elements (li, div)nth-child delay on block children✕ No
ease-split-textEach letter independentlynth-child delay on inline span per character✓ Yes
+
+ + + \ No newline at end of file diff --git a/submissions/examples/ease-split-text/style.css b/submissions/examples/ease-split-text/style.css new file mode 100644 index 0000000..621dc36 --- /dev/null +++ b/submissions/examples/ease-split-text/style.css @@ -0,0 +1,180 @@ +/* ============================================================ + EaseMotion CSS — Contributor Submission + Feature: Per-letter Staggered Entrance Animation + Folder: submissions/examples/ease-split-text/ + Author: AjayBandiwaddar + ============================================================ + + NAMING NOTE: + Class names here are contributor-defined. + The maintainer will rename to ease-* convention before merging. + + KEYFRAME NOTE: + This submission reuses ease-kf-fade-in and ease-kf-slide-up + already defined in core/animations.css — zero new keyframes + added to core. + ============================================================ */ + + +/* ── Design Tokens ───────────────────────────────────────── + All values reference --ease-* custom properties. + Hard-coded fallbacks are for standalone demo use only. + ──────────────────────────────────────────────────────────── */ + +:root { + --split-duration: 400ms; + --split-ease: var(--ease-ease-out, cubic-bezier(0, 0, 0.2, 1)); + --split-delay-step: 40ms; /* stagger gap between each letter */ + --split-rise: 16px; /* translateY travel for slide variant */ +} + + +/* ================================================================ + BASE: .split-text wrapper + Apply to any heading or text element. + Each direct child is one letter. + ================================================================ */ + +.split-text { + /* Prevent line wrapping from breaking the stagger sequence */ + display: block; +} + +/* Every letter span starts invisible */ +.split-text span { + display: inline-block; + opacity: 0; + /* Reuses ease-kf-fade-in from core/animations.css */ + animation-name: ease-kf-fade-in; + animation-duration: var(--split-duration); + animation-timing-function: var(--split-ease); + animation-fill-mode: forwards; +} + + +/* ================================================================ + SPACE PRESERVATION + Word spaces collapse in HTML — use .split-space to maintain them. + Usage: + ================================================================ */ + +.split-text .split-space { + display: inline-block; + width: 0.35em; + opacity: 1 !important; /* spaces are always visible */ + animation: none !important; +} + + +/* ================================================================ + nth-child STAGGER TABLE + Each letter gets an incremental animation-delay. + Step size: --split-delay-step (default 40ms). + Coverage: 30 letters — handles most heading lengths. + ================================================================ */ + +.split-text span:nth-child(1) { animation-delay: calc(var(--split-delay-step) * 0); } +.split-text span:nth-child(2) { animation-delay: calc(var(--split-delay-step) * 1); } +.split-text span:nth-child(3) { animation-delay: calc(var(--split-delay-step) * 2); } +.split-text span:nth-child(4) { animation-delay: calc(var(--split-delay-step) * 3); } +.split-text span:nth-child(5) { animation-delay: calc(var(--split-delay-step) * 4); } +.split-text span:nth-child(6) { animation-delay: calc(var(--split-delay-step) * 5); } +.split-text span:nth-child(7) { animation-delay: calc(var(--split-delay-step) * 6); } +.split-text span:nth-child(8) { animation-delay: calc(var(--split-delay-step) * 7); } +.split-text span:nth-child(9) { animation-delay: calc(var(--split-delay-step) * 8); } +.split-text span:nth-child(10) { animation-delay: calc(var(--split-delay-step) * 9); } +.split-text span:nth-child(11) { animation-delay: calc(var(--split-delay-step) * 10); } +.split-text span:nth-child(12) { animation-delay: calc(var(--split-delay-step) * 11); } +.split-text span:nth-child(13) { animation-delay: calc(var(--split-delay-step) * 12); } +.split-text span:nth-child(14) { animation-delay: calc(var(--split-delay-step) * 13); } +.split-text span:nth-child(15) { animation-delay: calc(var(--split-delay-step) * 14); } +.split-text span:nth-child(16) { animation-delay: calc(var(--split-delay-step) * 15); } +.split-text span:nth-child(17) { animation-delay: calc(var(--split-delay-step) * 16); } +.split-text span:nth-child(18) { animation-delay: calc(var(--split-delay-step) * 17); } +.split-text span:nth-child(19) { animation-delay: calc(var(--split-delay-step) * 18); } +.split-text span:nth-child(20) { animation-delay: calc(var(--split-delay-step) * 19); } +.split-text span:nth-child(21) { animation-delay: calc(var(--split-delay-step) * 20); } +.split-text span:nth-child(22) { animation-delay: calc(var(--split-delay-step) * 21); } +.split-text span:nth-child(23) { animation-delay: calc(var(--split-delay-step) * 22); } +.split-text span:nth-child(24) { animation-delay: calc(var(--split-delay-step) * 23); } +.split-text span:nth-child(25) { animation-delay: calc(var(--split-delay-step) * 24); } +.split-text span:nth-child(26) { animation-delay: calc(var(--split-delay-step) * 25); } +.split-text span:nth-child(27) { animation-delay: calc(var(--split-delay-step) * 26); } +.split-text span:nth-child(28) { animation-delay: calc(var(--split-delay-step) * 27); } +.split-text span:nth-child(29) { animation-delay: calc(var(--split-delay-step) * 28); } +.split-text span:nth-child(30) { animation-delay: calc(var(--split-delay-step) * 29); } + + +/* ================================================================ + VARIANT 1: Fade Only (default) + Letters appear in place — no movement. + Reuses ease-kf-fade-in from core/animations.css. + Already applied in the base .split-text rule above. + ================================================================ */ + +/* No extra rules needed — base .split-text IS the fade variant */ + + +/* ================================================================ + VARIANT 2: Slide Up + Letters rise 16px into position as they appear. + Reuses ease-kf-slide-up from core/animations.css + (from: opacity 0, translateY 24px → to: opacity 1, translateY 0). + We override with --split-rise for a subtler 16px travel. + ================================================================ */ + +.split-text-slide span { + transform: translateY(var(--split-rise)); + animation-name: ease-kf-slide-up; +} + + +/* ================================================================ + VARIANT 3: Fade + Blur + Letters materialize with a blur → sharp transition. + Adds depth without needing movement. + ================================================================ */ + +.split-text-blur span { + filter: blur(6px); + animation-name: split-kf-fade-blur; +} + +@keyframes split-kf-fade-blur { + to { + opacity: 1; + filter: blur(0); + } +} + + +/* ================================================================ + STAGGER SPEED PRESETS + Override --split-delay-step for faster or slower reveals. + ================================================================ */ + +/* Fast: 25ms per letter — punchy, short words */ +.split-text-fast { + --split-delay-step: 25ms; +} + +/* Slow: 70ms per letter — dramatic, impactful headings */ +.split-text-slow { + --split-delay-step: 70ms; +} + + +/* ================================================================ + ACCESSIBILITY: Reduced Motion + Show all letters immediately with no animation. + Text is never hidden from users who need reduced motion. + ================================================================ */ + +@media (prefers-reduced-motion: reduce) { + .split-text span { + opacity: 1 !important; + transform: none !important; + filter: none !important; + animation: none !important; + } +} \ No newline at end of file