diff --git a/submissions/examples/ease-dropdown/README.md b/submissions/examples/ease-dropdown/README.md new file mode 100644 index 0000000..95d0052 --- /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 ` + + + + + + + + + + + + + + + + + +
+

2 — Action menu with section labels & destructive item

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

3 — Position modifiers (.dropdown--right & .dropdown--up)

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

4 — Icon-only trigger (three-dot context menu)

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

Usage

+ +
<!-- 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 0000000..ec9411a --- /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, + + + + + + + + + + + + + + + + +``` + +### Semantic color variants + +Override `--tooltip-bg` for contextual meaning: ```html - + + + + + + + + + ``` -**Position Variants:** -- `.ease-tooltip-top` (or omit entirely, top is default) -- `.ease-tooltip-bottom` -- `.ease-tooltip-left` -- `.ease-tooltip-right` +```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 -### Why is it useful? -1. **Zero JS Overhead:** Because it utilizes HTML attributes and `content: attr()`, there is absolutely no JavaScript required to mount, position, or unmount the tooltip. This makes it incredibly lightweight and performant. -2. **Accessible by Default:** It respects `prefers-reduced-motion` flawlessly. Normally the tooltips have a smooth slide-in effect, but when reduced motion is enabled, the `transform` animation is stripped out while the `opacity` fade is preserved to prevent motion sickness. +- 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 index 5b6fb52..9027a8b 100644 --- a/submissions/examples/ease-tooltip/demo.html +++ b/submissions/examples/ease-tooltip/demo.html @@ -1,98 +1,406 @@ - - Tooltip Component Demo - - + + + ease-tooltip — EaseMotion CSS + + - - - - - + + + -

EaseMotion: ease-tooltip

-

A highly performant CSS-only tooltip system. Hover over the elements below to see the tooltips slide in elegantly.

+

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. +

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

Inline Text Usage

-

- You can also use tooltips on standard inline text. For example, hover over this - magic word - to learn more about it without using any Javascript. + + + +

+ +

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 index 697f805..6712d59 100644 --- a/submissions/examples/ease-tooltip/style.css +++ b/submissions/examples/ease-tooltip/style.css @@ -1,130 +1,276 @@ /* ============================================================ - EaseMotion CSS — ease-tooltip component (Proposal) - CSS-only tooltips using data-tooltip attributes. + 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. ============================================================ */ -/* ── Base Tooltip Container ────────────────────────────────── */ -.ease-tooltip { + +/* ── 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-block; - cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; } -/* ── Base Tooltip Bubble (::after) & Arrow (::before) ──────── */ -.ease-tooltip::after, -.ease-tooltip::before { + +/* ── Bubble (::before) ───────────────────────────────────── + Content is pulled from data-tip attribute. + ──────────────────────────────────────────────────────────── */ + +.tooltip::before { + content: attr(data-tip); position: absolute; - opacity: 0; - visibility: hidden; - pointer-events: none; - z-index: var(--ease-z-overlay); - transition: opacity var(--ease-speed-fast) var(--ease-ease), - transform var(--ease-speed-fast) var(--ease-ease), - visibility var(--ease-speed-fast) var(--ease-ease); -} - -/* Tooltip Bubble */ -.ease-tooltip::after { - content: attr(data-tooltip); - background-color: var(--ease-color-neutral-800); - color: var(--ease-color-surface); - padding: var(--ease-space-2) var(--ease-space-3); - border-radius: var(--ease-radius-md); - font-family: var(--ease-font-sans); - font-size: var(--ease-text-xs); + z-index: var(--ease-z-toast, 9999); + + /* Typography */ + font-size: var(--tooltip-font-size); font-weight: 500; + line-height: 1.4; white-space: nowrap; - box-shadow: var(--ease-shadow-md); + 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); } -/* Tooltip Arrow */ -.ease-tooltip::before { + +/* ── Arrow tip (::after) ─────────────────────────────────── + Built with a zero-size box + border trick. + ──────────────────────────────────────────────────────────── */ + +.tooltip::after { content: ''; - border: 5px solid transparent; + 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); } -/* Hover & Focus States (Show Tooltip) */ -.ease-tooltip:hover::after, -.ease-tooltip:hover::before, -.ease-tooltip:focus-visible::after, -.ease-tooltip:focus-visible::before { - opacity: 1; - visibility: visible; + +/* ================================================================ + 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); } -/* ── Positioning Variants ──────────────────────────────────── */ +.tooltip::before, +.tooltip-top::before { + bottom: calc(100% + var(--tooltip-offset)); + left: 50%; + transform: var(--_exit-transform); +} -/* TOP (Default) */ -.ease-tooltip-top::after, -.ease-tooltip:not([class*="ease-tooltip-"])::after { - bottom: 100%; - left: 50%; - transform: translate(-50%, -4px); +.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; } -.ease-tooltip-top::before, -.ease-tooltip:not([class*="ease-tooltip-"])::before { - bottom: 100%; - left: 50%; - transform: translate(-50%, 6px); - border-top-color: var(--ease-color-neutral-800); + +/* 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); } -.ease-tooltip-top:hover::after, -.ease-tooltip:not([class*="ease-tooltip-"]):hover::after { transform: translate(-50%, -8px); } -.ease-tooltip-top:hover::before, -.ease-tooltip:not([class*="ease-tooltip-"]):hover::before { transform: translate(-50%, 2px); } -/* BOTTOM */ -.ease-tooltip-bottom::after { - top: 100%; left: 50%; transform: translate(-50%, 4px); +.tooltip:hover::after, +.tooltip:focus-within::after, +.tooltip-top:hover::after, +.tooltip-top:focus-within::after { + opacity: 1; + transform: translateX(-50%) translateY(0); } -.ease-tooltip-bottom::before { - top: 100%; left: 50%; transform: translate(-50%, -6px); border-bottom-color: var(--ease-color-neutral-800); + + +/* ================================================================ + 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); } -.ease-tooltip-bottom:hover::after { transform: translate(-50%, 8px); } -.ease-tooltip-bottom:hover::before { transform: translate(-50%, -2px); } -/* RIGHT */ -.ease-tooltip-right::after { - top: 50%; left: 100%; transform: translate(4px, -50%); +.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); } -.ease-tooltip-right::before { - top: 50%; left: 100%; transform: translate(-6px, -50%); border-right-color: var(--ease-color-neutral-800); + +.tooltip-bottom:hover::before, +.tooltip-bottom:focus-within::before { + opacity: 1; + transform: translateX(-50%) translateY(0); } -.ease-tooltip-right:hover::after { transform: translate(8px, -50%); } -.ease-tooltip-right:hover::before { transform: translate(-2px, -50%); } -/* LEFT */ -.ease-tooltip-left::after { - top: 50%; right: 100%; transform: translate(-4px, -50%); +.tooltip-bottom:hover::after, +.tooltip-bottom:focus-within::after { + opacity: 1; + transform: translateX(-50%) translateY(0); } -.ease-tooltip-left::before { - top: 50%; right: 100%; transform: translate(6px, -50%); border-left-color: var(--ease-color-neutral-800); + + +/* ================================================================ + 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); } -.ease-tooltip-left:hover::after { transform: translate(-8px, -50%); } -.ease-tooltip-left:hover::before { transform: translate(2px, -50%); } -/* ── Accessibility: Reduced Motion ─────────────────────────── */ +/* ================================================================ + 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) { - .ease-tooltip::after, - .ease-tooltip::before { - /* Preserve opacity fades, but strip the sliding transforms */ - transition: opacity var(--ease-speed-fast) var(--ease-ease), - visibility var(--ease-speed-fast) var(--ease-ease) !important; + .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; } - - /* Reset hovers back to their original resting transforms, so they fade in place */ - .ease-tooltip-top:hover::after, - .ease-tooltip:not([class*="ease-tooltip-"]):hover::after { transform: translate(-50%, -4px) !important; } - .ease-tooltip-top:hover::before, - .ease-tooltip:not([class*="ease-tooltip-"]):hover::before { transform: translate(-50%, 6px) !important; } - - .ease-tooltip-bottom:hover::after { transform: translate(-50%, 4px) !important; } - .ease-tooltip-bottom:hover::before { transform: translate(-50%, -6px) !important; } - - .ease-tooltip-right:hover::after { transform: translate(4px, -50%) !important; } - .ease-tooltip-right:hover::before { transform: translate(-6px, -50%) !important; } - - .ease-tooltip-left:hover::after { transform: translate(-4px, -50%) !important; } - .ease-tooltip-left:hover::before { transform: translate(6px, -50%) !important; } -} +} \ No newline at end of file