mirror of
https://github.com/Nutlope/hallmark.git
synced 2026-08-14 12:35:33 +02:00
Worked-examples first-card aligns with header · skill: N10 Floating-on-scroll nav
Two changes in one pass — site polish + skill expansion. SITE: Worked-examples rail's first card now aligns with the section header on initial paint. Was: rail used `padding-inline: var(--page-gutter)` (~20 px), so Tide started flush against the viewport's left gutter — out of column with the "02 / EXAMPLES" label and "Worked examples." title above it. Now uses `padding-inline: max(var(--page-gutter), calc((100vw - var(--page-max)) / 2 + var(--page-gutter)))` so the first card's left edge matches the page-content edge. As the user scrolls horizontally, cards traverse the full bleed (right edge unchanged — still extends to viewport). Trailing `::after` spacer also uses the new `--rail-edge` so the last card snaps to matching content edge. SKILL: Added N10 Floating-on-scroll morph to the nav archetype catalogue plus a dedicated `references/floating-nav.md` recipe. The pattern: one DOM with two layers — `.nav` (outer) owns the default bar visuals, `.nav__inner` (inner) owns the floating pill visuals — cross-faded on a single `.is-floating` class toggle past a scroll threshold. Documents the four laws Hallmark refuses to break: 1. Total nav height stays constant (compensate inner shrink with outer padding-block math), or content below jitters. 2. Visible offset uses `transform: translateY()`, never margin or padding (those trigger layout and break Law 1). 3. Cross-fade ownership of every shared visual property (background, border, backdrop-filter, box-shadow). Forget one and you get ghost effects. 4. Single timing curve across every transitioning property — `cubic-bezier(0.16, 1, 0.3, 1)` at ~520 ms. Eight curves reads as eight animations. Plus the property morph table (10 properties, one curve), the scroll-handler script with passive listener + rAF throttle + boolean-flip guard, and an 8-item slop list (the AI-default mistakes Hallmark must not emit). N10's cookbook entry links to floating-nav.md and stays brief — the deep recipe lives in one focused reference file (~80 lines). Skill grew by ~95 lines; one new file, one updated entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
c517264e2d
commit
a729b1707f
+11
-7
@@ -198,22 +198,26 @@
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
scroll-snap-type: x mandatory;
|
||||
/* Full-bleed: escape both the page max-width AND the page gutter so
|
||||
the rail spans the full viewport regardless of how wide the
|
||||
screen is. Cards extend right up to the viewport's edge. */
|
||||
/* Full-bleed: rail spans the full viewport. But the FIRST card
|
||||
starts aligned with the page content (same left edge as the
|
||||
section header), not flush against the viewport. As the user
|
||||
scrolls horizontally, cards travel through the full bleed
|
||||
including the viewport edges — only the initial position
|
||||
respects the page's content gutter. */
|
||||
width: 100vw;
|
||||
margin-inline: calc(50% - 50vw);
|
||||
padding-inline: var(--page-gutter);
|
||||
--rail-edge: max(var(--page-gutter), calc((100vw - var(--page-max)) / 2 + var(--page-gutter)));
|
||||
padding-inline: var(--rail-edge);
|
||||
padding-block: var(--space-md) var(--space-lg);
|
||||
scroll-padding-inline: var(--page-gutter);
|
||||
scroll-padding-inline: var(--rail-edge);
|
||||
scrollbar-color: var(--color-rule-2) transparent;
|
||||
}
|
||||
|
||||
/* Reserve trailing whitespace inside the rail so the last card snaps
|
||||
clean to the gutter and isn't cut off by the bleed margin. */
|
||||
clean to the same content edge as the first. */
|
||||
.examples-rail::after {
|
||||
content: "";
|
||||
flex: 0 0 var(--page-gutter);
|
||||
flex: 0 0 var(--rail-edge);
|
||||
}
|
||||
|
||||
.examples-rail::-webkit-scrollbar { height: 6px; }
|
||||
|
||||
@@ -876,6 +876,24 @@ Wordmark hard-left, single CTA hard-right, vast empty space between, no link row
|
||||
|
||||
*Anti-pattern:* adding 4 inline links between the wordmark and CTA "to fill the space". The space *is* the design; if you fill it, you've made N1 with extra steps.
|
||||
|
||||
### N10 · Floating-on-scroll morph
|
||||
A sticky bar at the top that **morphs into a floating pill** as the user scrolls past a threshold. Two visual modes share one DOM — `.nav` (outer) owns the bar look, `.nav__inner` (inner) owns the pill look. Cross-faded on a single class toggle (`.is-floating`) with one timing curve. Active layer feels seamless; AI defaults always botch this.
|
||||
*Use when:* atmospheric / modern-minimal pages where the kinetic micro-moment earns its place. Adds a single tasteful surprise; resists novelty.
|
||||
*Don't confuse with:* N5 Floating pill (always-on, no scroll behaviour). N10 is N5 plus a default-bar state that morphs *into* it.
|
||||
|
||||
```html
|
||||
<header class="nav">
|
||||
<div class="nav__inner">
|
||||
<a class="wordmark">Hallmark</a>
|
||||
<ul class="nav__links">…</ul>
|
||||
</div>
|
||||
</header>
|
||||
```
|
||||
|
||||
The full recipe — the four laws (height-constant, transform-for-offset, cross-fade-everything, single-curve), the property-morph table, the scroll-handler script, and the eight anti-patterns Hallmark refuses — lives in [`floating-nav.md`](floating-nav.md). Reach for that file *before* building this archetype. Skipping the four laws is what makes 90% of attempts read as broken.
|
||||
|
||||
*Anti-pattern (one of eight in floating-nav.md):* swapping two `<header>` elements via opacity instead of cross-fading one DOM. Doubles markup, fights focus order, desyncs content.
|
||||
|
||||
---
|
||||
|
||||
## Routing — which nav fits which genre / theme
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Floating nav on scroll — the cross-fade morph
|
||||
|
||||
The recipe for **N10 · Floating-on-scroll morph** (see [`component-cookbook.md` § Navigation](component-cookbook.md)). One DOM, two visual modes, single class toggle, one timing curve. AI defaults botch every one of the four laws below — which is why N10 is the most demanding nav in the cookbook.
|
||||
|
||||
## The structure
|
||||
|
||||
One `<header>` with an inner wrapper. The outer owns the **default bar** visuals; the inner owns the **floating pill** visuals. As `.is-floating` toggles past a scroll threshold, each layer cross-fades its own visuals out while the other fades in.
|
||||
|
||||
```html
|
||||
<header class="nav">
|
||||
<div class="nav__inner">
|
||||
<a class="wordmark">Hallmark</a>
|
||||
<ul class="nav__links">…</ul>
|
||||
<a class="cta">Install</a>
|
||||
</div>
|
||||
</header>
|
||||
```
|
||||
|
||||
## The four laws — non-negotiable
|
||||
|
||||
**1. Total nav height stays constant.** If outer height changes when state flips, every pixel below shifts vertically. Users perceive this as "the page jumped" mid-scroll. Compensate the inner's shrink with the outer's `padding-block` so the math sums to the same total in both states.
|
||||
|
||||
**2. Visible offset uses `transform: translateY()`, never `padding`/`margin`.** The pill should sit detached from the viewport top with breathing room. The naïve add-padding-to-outer fix breaks Law 1. `transform` doesn't affect layout — pill drops visually, box stays put.
|
||||
|
||||
**3. Cross-fade ownership of every shared visual.** For every property the outer carries in default (background, border, backdrop-filter, box-shadow), explicitly neutralise it in `.is-floating` *and* put it in the transition list. Forgetting one — say a stale `backdrop-filter: blur(14px)` on the outer in floating mode — yields an invisible blurred strip dragging across the viewport behind nothing.
|
||||
|
||||
**4. Single timing curve across every property.** Eight properties on eight curves reads as eight animations. Same eight on `var(--dur-mid)` + `var(--ease-out)` reads as one motion. Use `cubic-bezier(0.16, 1, 0.3, 1)` (exponential ease-out) and ~520 ms.
|
||||
|
||||
## The property morph (10 properties, one curve)
|
||||
|
||||
| Element | Property | Default | Floating |
|
||||
|---|---|---|---|
|
||||
| `.nav` | `padding-block` | `0` | `var(--space-2xs)` |
|
||||
| `.nav` | `background-color` | dark/0.62 | `transparent` |
|
||||
| `.nav` | `border-block-end-color` | rule | `transparent` |
|
||||
| `.nav` | `backdrop-filter` | `saturate(1.4) blur(14px)` | `blur(0)` |
|
||||
| `.nav__inner` | `max-width` | `var(--page-max)` | `~58rem` |
|
||||
| `.nav__inner` | `min-height` | `60px` | `52px` |
|
||||
| `.nav__inner` | `padding-block` | `12px` | `4px` |
|
||||
| `.nav__inner` | `border-radius` | `0` | `var(--radius-pill, 999px)` |
|
||||
| `.nav__inner` | `background-color` | `transparent` | dark/0.82 |
|
||||
| `.nav__inner` | `backdrop-filter` | `blur(0)` | `blur(18px)` |
|
||||
| `.nav__inner` | `box-shadow` | `none` | drop + tinted glow + inset hairline |
|
||||
| `.nav__inner` | `transform` | `translateY(0)` | `translateY(12px)` |
|
||||
|
||||
Use `blur(0)` not `none` — `none` snaps, `blur(0)` transitions. `backdrop-filter` transitions are 2024+ baseline (Chrome 107+, Safari 14+, Firefox 103+).
|
||||
|
||||
## The scroll handler
|
||||
|
||||
```js
|
||||
(() => {
|
||||
const nav = document.querySelector(".nav");
|
||||
if (!nav) return;
|
||||
const THRESHOLD = 80; // ≥ 60 px to avoid micro-scroll twitches
|
||||
let floating = false;
|
||||
let ticking = false;
|
||||
const update = () => {
|
||||
const next = window.scrollY > THRESHOLD;
|
||||
if (next !== floating) { // boolean-flip guard — toggle once per state change
|
||||
floating = next;
|
||||
nav.classList.toggle("is-floating", floating);
|
||||
}
|
||||
};
|
||||
window.addEventListener("scroll", () => {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(() => { update(); ticking = false; });
|
||||
}, { passive: true }); // mobile scroll-perf — keep main thread free
|
||||
update();
|
||||
})();
|
||||
```
|
||||
|
||||
Three discipline points:
|
||||
- **`passive: true`** — listener won't `preventDefault`, browser keeps main thread free for scrolling.
|
||||
- **`requestAnimationFrame` throttle** — caps to 60 calls/s, aligns with paint.
|
||||
- **Boolean-flip guard** — class operation runs once per state change, not once per scroll event.
|
||||
|
||||
## Anti-patterns Hallmark refuses
|
||||
|
||||
1. Two separate `<header>` elements that swap via opacity. Doubles DOM, fights focus order, can desync content.
|
||||
2. Animating `top` / `margin-top` to add the floating offset. Triggers layout. `transform` is the only correct lever.
|
||||
3. Using `backdrop-filter: none` in the floating state. Snaps. Use `blur(0)` so it transitions.
|
||||
4. Letting nav height change between states. Causes content jitter — the single most damaging mistake in this pattern.
|
||||
5. Different transition durations per property ("speed up the radius", "delay the shadow"). Reads as broken even though everything moves. One curve.
|
||||
6. `scroll` event without `{ passive: true }` or without rAF throttling. Both kill mobile scroll perf.
|
||||
7. Threshold = 0 (morph fires at the slightest scroll). Too jumpy. Use ≥ 60 px.
|
||||
8. Forgetting the boolean-flip guard — toggling the class on every scroll event causes layout thrash.
|
||||
|
||||
The pattern works because it's restrained — one orchestrated morph, no embellishment. Pour the polish into the timing curve, the shadow stack, and the height-constant math; not into "more."
|
||||
Reference in New Issue
Block a user