From d6d2997d39b6860be48442dcb8e361493a48e8df Mon Sep 17 00:00:00 2001 From: Ajay Bandiwadar Date: Tue, 2 Jun 2026 06:32:25 +0530 Subject: [PATCH 1/5] 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 00000000..b81d7694 --- /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 00000000..9027a8b2 --- /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 00000000..6712d59f --- /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/5] 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 00000000..7bcfdd64 --- /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 00000000..93205141 --- /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 00000000..1e8cb89d --- /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/5] 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 00000000..0ce122db --- /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 00000000..28d4298e --- /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 00000000..621dc36b --- /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 From 094c56224f3e5db2cc0af851f3498d7012845fe2 Mon Sep 17 00:00:00 2001 From: Ajay Bandiwadar Date: Tue, 2 Jun 2026 07:00:57 +0530 Subject: [PATCH 4/5] feat: add ease-dropdown CSS-only animated dropdown menu component --- submissions/examples/ease-dropdown/README.md | 58 +++ submissions/examples/ease-dropdown/demo.html | 341 +++++++++++++++++ submissions/examples/ease-dropdown/style.css | 363 +++++++++++++++++++ 3 files changed, 762 insertions(+) create mode 100644 submissions/examples/ease-dropdown/README.md create mode 100644 submissions/examples/ease-dropdown/demo.html create mode 100644 submissions/examples/ease-dropdown/style.css diff --git a/submissions/examples/ease-dropdown/README.md b/submissions/examples/ease-dropdown/README.md new file mode 100644 index 00000000..95d0052c --- /dev/null +++ b/submissions/examples/ease-dropdown/README.md @@ -0,0 +1,58 @@ +# ease-dropdown + +## What does this do? + +Provides a floating dropdown menu that opens on `:focus-within` — no JavaScript required. The panel fades in while scaling from its top edge, giving a natural top-down unfold. It closes automatically when focus leaves the trigger. + +## How is it used? + +```html + + +``` + +### Available classes + +| Class | Purpose | +|---|---| +| `.dropdown` | Required wrapper — carries `:focus-within` state | +| `.dropdown-trigger` | Applied to the button/link that opens the panel | +| `.dropdown-chevron` | CSS-drawn rotating arrow icon inside the trigger | +| `.dropdown-menu` | The floating panel | +| `.dropdown-item` | A single menu item (`` or ` + + +
+ + + + + + + + + + + + + + +
+ + +
+ + + +
+
+ + + +
+ + +
+ + + + + + + +
+
+ + + +
+ + +
+ + + +
+
+ + + +
+ + +
<!-- Basic dropdown -->
+<div class="dropdown">
+  <button class="ease-btn ease-btn-primary dropdown-trigger"
+          aria-haspopup="menu" type="button">
+    Options
+    <span class="dropdown-chevron" aria-hidden="true"></span>
+  </button>
+
+  <ul class="dropdown-menu" role="menu">
+    <li role="none"><a class="dropdown-item" href="#" role="menuitem" tabindex="-1">Edit</a></li>
+    <li role="none"><a class="dropdown-item" href="#" role="menuitem" tabindex="-1">Duplicate</a></li>
+    <li class="dropdown-divider" role="separator"></li>
+    <li role="none"><a class="dropdown-item dropdown-item-danger" href="#" role="menuitem" tabindex="-1">Delete</a></li>
+  </ul>
+</div>
+
+<!-- Right-aligned panel -->
+<div class="dropdown dropdown--right"> … </div>
+
+<!-- Opens upward -->
+<div class="dropdown dropdown--up"> … </div>
+
+<!-- Wider panel (240px) -->
+<div class="dropdown dropdown--wide"> … </div>
+
+ + + + + \ No newline at end of file diff --git a/submissions/examples/ease-dropdown/style.css b/submissions/examples/ease-dropdown/style.css new file mode 100644 index 00000000..ec9411ac --- /dev/null +++ b/submissions/examples/ease-dropdown/style.css @@ -0,0 +1,363 @@ +/* ============================================================ + SUBMISSION — ease-dropdown + CSS-only animated dropdown menu component. + Opens via :focus-within (no JavaScript required). + Closes automatically when focus leaves the trigger. + + Variants: + .dropdown Base wrapper + .dropdown-menu Floating panel + .dropdown-item Menu item link / button + .dropdown-item-danger Destructive item (red) + .dropdown-item-muted Disabled-looking item + .dropdown-divider Visual separator rule + .dropdown--right Align panel to the right edge + .dropdown--up Open upward instead of downward + .dropdown--wide 240 px min-width panel + + All values reference the EaseMotion CSS token layer + (core/variables.css). Fallback literals are provided so + the file also works as a standalone demo without the + framework loaded. + ============================================================ */ + + +/* ── 0. Demo-page chrome (not part of the component) ──────── */ + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + min-height: 100vh; + display: grid; + place-items: center; + padding: 60px 24px 80px; + font-family: var(--ease-font-sans, 'Inter', system-ui, -apple-system, sans-serif); + font-size: var(--ease-text-base, 1rem); + line-height: var(--ease-leading-normal, 1.6); + color: var(--ease-color-neutral-800, #1e293b); + background-color: var(--ease-color-bg, #f8fafc); + -webkit-font-smoothing: antialiased; +} + +.demo-shell { + width: min(860px, 100%); + display: flex; + flex-direction: column; + gap: 56px; +} + +/* Page header */ +.demo-header { + display: flex; + flex-direction: column; + gap: 8px; +} + +.demo-eyebrow { + font-size: var(--ease-text-xs, 0.75rem); + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ease-color-primary, #6c63ff); +} + +.demo-header h1 { + font-size: clamp(1.75rem, 5vw, 2.5rem); + font-weight: 700; + line-height: var(--ease-leading-tight, 1.25); + color: var(--ease-color-neutral-900, #0f172a); +} + +.demo-header p { + max-width: 52ch; + color: var(--ease-color-muted, #64748b); + font-size: var(--ease-text-sm, 0.875rem); + margin: 0; +} + +/* Section labels */ +.demo-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.demo-section-label { + font-size: var(--ease-text-xs, 0.75rem); + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ease-color-neutral-400, #94a3b8); +} + +/* Row that holds one or more dropdowns side by side */ +.demo-row { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 16px; +} + +/* Usage code block */ +.demo-code { + background: var(--ease-color-neutral-900, #0f172a); + color: var(--ease-color-neutral-200, #e2e8f0); + border-radius: var(--ease-radius-md, 0.5rem); + padding: var(--ease-space-4, 1rem) var(--ease-space-6, 1.5rem); + font-family: var(--ease-font-mono, 'JetBrains Mono', 'Fira Code', monospace); + font-size: var(--ease-text-sm, 0.875rem); + line-height: 1.7; + overflow-x: auto; + white-space: pre; +} + + +/* ── 1. Dropdown wrapper ───────────────────────────────────── */ + +.dropdown { + position: relative; + display: inline-block; +} + + +/* ── 2. Trigger button ─────────────────────────────────────── */ + +/* + Works with any element (.ease-btn, + + +
+ + Something went wrong. +
+ + +``` + +**Optional stack placement modifiers** + +```html +
+
+
+ +``` + +**Optional two-line toast (title + body)** + +```html +
+ + + Session expiring + You will be logged out in 5 minutes. + +
``` -#### Theme Modifiers -- **Success Accent**: `.ease-toast-success` -- **Danger Accent**: `.ease-toast-danger` -- **Warning Accent**: `.ease-toast-warning` -- **Info Accent**: `.ease-toast-info` +**Minimal JS helper (copy-paste ready)** -#### Custom CSS Variables Configuration -You can customize the toast appearance dynamically: +```js +function dismissToast(toast) { + if (!toast || toast.classList.contains('toast--dismissing')) return; + toast.classList.add('toast--dismissing'); + setTimeout(() => toast.remove(), 350); +} -```css -.my-custom-toast { - --toast-bg: #090c14; - --toast-color: #f8fafc; - --toast-accent: #a855f7; /* Custom purple accent border */ +function fireToast(variant, message, duration = 4000) { + const icons = { success: '✓', danger: '✕', warning: '⚠', info: 'ℹ' }; + const stack = document.querySelector('.toast-stack'); + const el = document.createElement('div'); + el.className = `toast toast-${variant} ease-slide-in-right`; + el.style.setProperty('--toast-duration', `${duration}ms`); + el.setAttribute('role', 'alert'); + el.innerHTML = ` + ${icons[variant]} + ${message} + + `; + stack.appendChild(el); + setTimeout(() => dismissToast(el), duration); } ``` ---- +## 3. Why is it useful? + +Toasts are one of the most animation-heavy UI patterns in any product — every project needs them and every project currently builds them from scratch. + +This submission fits EaseMotion's philosophy in two specific ways: -### Why does it fit EaseMotion CSS? +**Zero new keyframes.** Entrance animation is a single composable class (`ease-slide-in-right`, `ease-fade-in`, `ease-slide-up` — developer's choice). Exit animation is handled by the `.toast--dismissing` state, which reverses the same keyframes already in `animations.css`. The component itself is pure structure and color. -A dynamic toast notification is a key UI pattern missing from the components library. +**Token-aware by default.** Every color value references a semantic alias (`--ease-color-surface`, `--ease-color-success`, `--ease-color-muted`, etc.), so toasts inherit the dark-mode overrides from `variables.css` automatically — no extra dark-mode CSS required. -This implementation provides both the entry slide-in transition and the auto-dismiss exit collapse sequence in **pure CSS** (using a delayed exit animation with `animation-fill-mode: forwards`). It requires zero JavaScript runtime for animation logic, supporting EaseMotion's philosophy of highly performant, lightweight, and human-readable styling. +The auto-dismiss progress bar uses a single `scaleX` keyframe driven by a CSS custom property (`--toast-duration`), making the dismiss duration controllable per-toast without JavaScript. diff --git a/submissions/examples/toast-notification/demo.html b/submissions/examples/toast-notification/demo.html index 239b3b14..5ea07cac 100644 --- a/submissions/examples/toast-notification/demo.html +++ b/submissions/examples/toast-notification/demo.html @@ -3,199 +3,311 @@ - Toast Notification Component Demo — EaseMotion CSS - - - - - - - - - - - - - - - - + Toast Notification Component – EaseMotion CSS + + + + + + + - - - -
- -
- - -
-
-

Toast Notification

-

- A premium, slide-in toast component supporting themed color accents, stacking spacing, and pure CSS auto-dismiss animations. -

-
-
- - -
- - -
-

1. Standard Variants

-

- Click the buttons below to spawn styled alerts. They will slide in from the top-right corner and auto-dismiss after 3 seconds. -

- -
- - - - - - - -
-
+ - -
-

2. Customizer & Stacking Simulator

-

- Trigger multiple toasts to verify vertical stacking. When a toast is dismissed, the lower items slide up automatically. -

+
- + +
+

EaseMotion CSS · Submission

+

Toast Notifications

+

Temporary feedback messages with entrance animation, auto-dismiss, and a live progress bar. No new keyframes — built entirely on existing EaseMotion animation classes.

+
-
-
- - -
-
- -
- - -
-
-
+ +
+

Fire a toast

+
+ + + +
+
+ +
+

Stack position

+
+ + + + +
- -
-

3. Javascript Integration Guide

-

- To manage mounting and cleanup of toasts in standard web applications, developers can use a simple helper like the one below: -

- -
-function showToast(message, type = 'info') {
-  // Find or create container
-  let container = document.querySelector('.ease-toast-container');
-  if (!container) {
-    container = document.createElement('div');
-    container.className = 'ease-toast-container';
-    document.body.appendChild(container);
-  }
-
-  // Create toast element
-  const toast = document.createElement('div');
-  toast.className = `ease-toast ease-toast-${type}`;
-  toast.innerHTML = `<span>\${message}</span>`;
-
-  // Mount
-  container.appendChild(toast);
-
-  // Auto-remove element from DOM after the exit animation completes (3.7s)
-  setTimeout(() => {
-    toast.remove();
-  }, 3700);
-}
+ +
+

How to use

+
<!-- Stack container -->
+<div class="toast-stack">
+
+  <!-- Add ease-slide-in-right for entrance -->
+  <div class="toast toast-success ease-slide-in-right">
+    <span class="toast-icon"></span>
+    <span class="toast-message">Changes saved.</span>
+    <button class="toast-close"></button>
+  </div>
+
+</div>
- + +
+ + // mark default active on load + document.getElementById('default-pos').classList.add('active'); + diff --git a/submissions/examples/toast-notification/style.css b/submissions/examples/toast-notification/style.css index 6dfb39cd..359155d5 100644 --- a/submissions/examples/toast-notification/style.css +++ b/submissions/examples/toast-notification/style.css @@ -1,222 +1,169 @@ /* ============================================================ - EaseMotion CSS — Toast Notification Component - Sliding, auto-dismissing, stackable notifications + Toast / Notification Component + Composable toast system that delegates all entrance/exit + animation to existing EaseMotion animation classes. + No keyframes defined here — just structure, color, layout. ============================================================ */ -/* ── 1. Core Toast Styles ──────────────────────────────────── */ - -/* Stacking Container */ -.ease-toast-container { +/* ── Stack container ──────────────────────────────────────── + Fixed to bottom-right by default. Toasts stack upward. + ──────────────────────────────────────────────────────── */ +.toast-stack { position: fixed; - top: var(--ease-space-6, 24px); - right: var(--ease-space-6, 24px); + bottom: 1.5rem; + right: 1.5rem; display: flex; + flex-direction: column-reverse; /* newest toast at bottom, closest to corner */ + gap: 0.625rem; + z-index: var(--ease-z-toast, 9999); + max-width: 360px; + width: calc(100vw - 3rem); /* safe on narrow viewports */ + pointer-events: none; /* stack itself is transparent to clicks */ +} + +/* ── Stack placement modifiers ──────────────────────────── */ +.toast-stack--top-right { + bottom: auto; + top: 1.5rem; + right: 1.5rem; flex-direction: column; - gap: var(--ease-space-3, 12px); - z-index: 9999; - pointer-events: none; /* Let clicks pass through empty spaces */ - box-sizing: border-box; -} - -/* Individual Toast Box */ -.ease-toast { - pointer-events: auto; /* Re-enable pointer events for interactions */ - background-color: var(--toast-bg, #1e293b); - color: var(--toast-color, #f8fafc); - border-left: 4px solid var(--toast-accent, var(--ease-color-primary, #6366f1)); - border-radius: var(--ease-radius-md, 6px); - padding: var(--ease-space-4, 16px) var(--ease-space-5, 20px); - box-shadow: var(--ease-shadow-lg); - +} + +.toast-stack--top-left { + bottom: auto; + top: 1.5rem; + right: auto; + left: 1.5rem; + flex-direction: column; +} + +.toast-stack--bottom-left { + right: auto; + left: 1.5rem; +} + +/* ── Base toast ───────────────────────────────────────────── + Uses semantic tokens so it respects dark-mode overrides. + ──────────────────────────────────────────────────────── */ +.toast { display: flex; - align-items: center; - gap: var(--ease-space-3, 12px); - - min-width: 300px; - max-width: 400px; - box-sizing: border-box; - - /* Make sure max-height accommodates the content during slide in */ - max-height: 120px; + align-items: flex-start; + gap: 0.75rem; + padding: 0.875rem 1rem; + border-radius: var(--ease-radius-lg, 10px); + border-left: 4px solid transparent; + box-shadow: var(--ease-shadow-lg); + background: var(--ease-color-surface, #ffffff); + color: var(--ease-color-text, #1e293b); + font-family: var(--ease-font-sans); + font-size: var(--ease-text-sm, 0.875rem); + font-weight: 500; + line-height: var(--ease-leading-normal, 1.6); + pointer-events: all; /* individual toasts ARE clickable */ + position: relative; overflow: hidden; +} - /* Run slide-in, delay, then dismiss */ - animation: - ease-toast-slide-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards, - ease-toast-dismiss 0.4s cubic-bezier(0.16, 1, 0.3, 1) 3.3s forwards; -} - -/* ── 2. Keyframes ───────────────────────────────────────────── */ - -/* Slide-in from right margin */ -@keyframes ease-toast-slide-in { - from { - transform: translateX(120%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* Slide-out with vertical height collapse to allow smooth stacking shifts */ -@keyframes ease-toast-dismiss { - 0% { - transform: translateX(0); - opacity: 1; - max-height: 120px; - padding-top: var(--ease-space-4, 16px); - padding-bottom: var(--ease-space-4, 16px); - margin-bottom: 0; - } - 90% { - transform: translateX(120%); - opacity: 0; - max-height: 120px; - padding-top: var(--ease-space-4, 16px); - padding-bottom: var(--ease-space-4, 16px); - margin-bottom: 0; - } - 100% { - transform: translateX(120%); - opacity: 0; - max-height: 0; - padding-top: 0; - padding-bottom: 0; - margin-top: 0; - margin-bottom: -12px; /* Pulls up adjacent elements to offset container gap */ - border: 0; - } -} - -/* ── 3. Theme Variants ──────────────────────────────────────── */ - -.ease-toast-success { - --toast-accent: var(--ease-color-success, #22c55e); -} - -.ease-toast-danger { - --toast-accent: var(--ease-color-danger, #ef4444); -} - -.ease-toast-warning { - --toast-accent: var(--ease-color-warning, #f59e0b); -} - -.ease-toast-info { - --toast-accent: var(--ease-color-primary, #6366f1); -} - -/* ── 4. Accessibility (Reduced Motion) ─────────────────────── */ - -@media (prefers-reduced-motion: reduce) { - .ease-toast { - transform: none !important; - animation: - ease-toast-fade-in 0.3s ease forwards, - ease-toast-fade-out 0.4s ease 3.3s forwards; - } -} - -@keyframes ease-toast-fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes ease-toast-fade-out { - 0% { - opacity: 1; - max-height: 120px; - padding-top: var(--ease-space-4, 16px); - padding-bottom: var(--ease-space-4, 16px); - margin-bottom: 0; - } - 90% { - opacity: 0; - max-height: 120px; - padding-top: var(--ease-space-4, 16px); - padding-bottom: var(--ease-space-4, 16px); - margin-bottom: 0; - } - 100% { - opacity: 0; - max-height: 0; - padding-top: 0; - padding-bottom: 0; - margin-top: 0; - margin-bottom: -12px; - border: 0; - } -} - -/* ── 5. Demo Page Styles (Self-contained) ──────────────────── */ - -:root { - --color-bg: #0b0f19; - --color-card: #151d30; - --color-border: #273552; - --text-primary: #f8fafc; - --text-secondary: #94a3b8; -} - -body { - background-color: var(--color-bg); - color: var(--text-primary); - font-family: 'Inter', sans-serif; - margin: 0; - min-height: 100vh; -} - -.showcase-card { - background: var(--color-card); - border: 1px solid var(--color-border); - border-radius: var(--ease-radius-lg); - padding: var(--ease-space-6); - box-sizing: border-box; -} - -.code-preview-box { - background: #07090f; - border: 1px solid var(--color-border); - padding: var(--ease-space-4); - border-radius: var(--ease-radius-md); - font-family: 'JetBrains Mono', monospace; - font-size: var(--ease-text-xs); - color: #a78bfa; - margin: 0; - overflow-x: auto; +/* ── Progress bar (auto-dismiss countdown) ──────────────── */ +.toast::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + height: 3px; + width: 100%; + background: currentColor; + opacity: 0.25; + transform-origin: left; + animation: toast-progress var(--toast-duration, 4000ms) linear forwards; } -.customizer-form { - display: flex; - flex-direction: column; - gap: var(--ease-space-4); +@keyframes toast-progress { + from { transform: scaleX(1); } + to { transform: scaleX(0); } } -.form-group { - display: flex; - flex-direction: column; - gap: var(--ease-space-1); +/* ── Icon ───────────────────────────────────────────────── */ +.toast-icon { + font-size: 1.1rem; + flex-shrink: 0; + line-height: 1.5; /* align with first text line */ +} + +/* ── Message ────────────────────────────────────────────── */ +.toast-message { + flex: 1; +} + +.toast-title { + font-weight: 700; + margin-bottom: 0.125rem; +} + +.toast-body { + opacity: 0.8; + font-size: var(--ease-text-xs, 0.75rem); +} + +/* ── Close button ───────────────────────────────────────── */ +.toast-close { + background: none; + border: none; + cursor: pointer; + font-size: 0.875rem; + line-height: 1; + opacity: 0.4; + padding: 0; + flex-shrink: 0; + color: inherit; + transition: opacity var(--ease-speed-fast, 150ms) var(--ease-ease); + align-self: flex-start; + margin-top: 0.15rem; +} + +.toast-close:hover { opacity: 1; } +.toast-close:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + border-radius: 2px; + opacity: 1; +} + +/* ── Color variants ───────────────────────────────────────── + Uses raw palette values so variants work even before the + maintainer swaps them for var(--ease-color-*) aliases. + ──────────────────────────────────────────────────────── */ +.toast-success { + border-left-color: var(--ease-color-success, #22c55e); + background: color-mix(in srgb, var(--ease-color-success, #22c55e) 8%, var(--ease-color-surface, #fff)); + color: var(--ease-color-success-dark, #15803d); +} + +.toast-danger { + border-left-color: var(--ease-color-danger, #ef4444); + background: color-mix(in srgb, var(--ease-color-danger, #ef4444) 8%, var(--ease-color-surface, #fff)); + color: var(--ease-color-danger-dark, #b91c1c); } -.form-label { - font-size: var(--ease-text-xs); - font-weight: 600; - color: var(--text-secondary); +.toast-warning { + border-left-color: var(--ease-color-warning, #f59e0b); + background: color-mix(in srgb, var(--ease-color-warning, #f59e0b) 8%, var(--ease-color-surface, #fff)); + color: var(--ease-color-warning-dark, #b45309); } -.text-input { - background: #090c14; - border: 1px solid var(--color-border); - border-radius: var(--ease-radius-md); - padding: var(--ease-space-3); - color: var(--text-primary); - font-family: inherit; - font-size: var(--ease-text-sm); +.toast-info { + border-left-color: var(--ease-color-primary, #6c63ff); + background: color-mix(in srgb, var(--ease-color-primary, #6c63ff) 8%, var(--ease-color-surface, #fff)); + color: var(--ease-color-primary-dark, #4b44cc); } -.text-input:focus { - outline: 2px solid var(--ease-color-primary); - border-color: transparent; +/* ── Exit state ───────────────────────────────────────────── + JS adds .toast--dismissing; paired with ease-fade-out or + ease-slide-in-right reversed via animation-direction. + ──────────────────────────────────────────────────────── */ +.toast--dismissing { + animation: + ease-kf-slide-in-right var(--ease-speed-medium, 300ms) var(--ease-ease) reverse forwards, + ease-kf-fade-out var(--ease-speed-medium, 300ms) var(--ease-ease) forwards; + pointer-events: none; }