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:
Youssef
2026-05-06 14:28:34 +01:00
co-authored by Claude Opus 4.7
parent c517264e2d
commit a729b1707f
3 changed files with 118 additions and 7 deletions
+11 -7
View File
@@ -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; }
+18
View File
@@ -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
+89
View File
@@ -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."