[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6D21SP7BQD0HP6YBKP9D4VE":3,"skill-same:01M6D21SP7BQD0HP6YBKP9D4VE":57},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":29,"linkOnly":30,"markdownContent":31,"markdownBody":32,"frontMatter":33,"refName":34,"commitSha":35,"skillDirectory":36,"forksCount":37,"repositoryHtmlUrl":38,"repositoryHomepageUrl":39,"ownerHtmlUrl":40,"ownerBlogUrl":39,"ownerDescription":41,"updatedAt":42,"install":43,"resources":56},"01M6D21SP7BQD0HP6YBKP9D4VE","motion-reveal",null,"Scroll-triggered entrances and scroll-linked effects: one observer for the page, a final state that shows without JavaScript or motion, staggering by arrival, scroll-driven CSS, parallax and pinned sections done carefully, the section marker in a nav, smooth anchor scrolling. Read before animating anything on scroll.","enowdev\u002Fenowxcli","enowdev","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F179420349?v=4","01M6D1MVX2KC0X3Z9NESE7P22K","Enow Dev.","01M6D1MW1FMZGBDKWAJY4GQX81",67,"Apache-2.0","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fmotion-reveal\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28],"motion","motion reveal","css","javascript","state","page","read","done","scroll","anchor","2026-11-10 10:12:30",false,"---\nname: motion-reveal\ndescription: \"Scroll-triggered entrances and scroll-linked effects: one observer for the page, a final state that shows without JavaScript or motion, staggering by arrival, scroll-driven CSS, parallax and pinned sections done carefully, the section marker in a nav, smooth anchor scrolling. Read before animating anything on scroll.\"\n---\n\n# Motion on scroll\n\nThe generated version: every section fades up 40px as it enters, the same\nway on every page, and the page is blank when the script is late. This is\nhow content arrives in a way that says something, for one observer, never\nhidden from someone who cannot or will not see it move. Principles in\n`motion`, timing in `motion-timing`, cost in `motion-performance`.\n\n## 1. When to reveal on scroll\n\n- At dial 2 and up (`motion`), on pages people read once or now and then:\n  a launch page, a product site, a portfolio, a story, a report.\n- Not on dashboards, application screens, docs, forms, settings, search\n  results, pages used every day, or content scanned for one fact.\n- Reveal a block when its arrival says something: an order (steps in turn),\n  a cause (a line drawn to what it connects), a group arriving together.\n  The rest can just be there; a page where everything fades in has no\n  emphasis left.\n- The first screen never waits for a scroll: it plays its entrance on load,\n  or is simply there.\n\n## 2. The pattern: one observer, reveal once\n\n- Each block that enters carries `data-reveal`. One IntersectionObserver\n  for the page sets `data-shown` on it the first time it comes into view,\n  then stops watching it.\n- The root carries `data-motion`, set before the first paint, only when\n  this can work (the observer exists, motion is welcome).\n- The hidden pre-state exists only under `[data-motion]` inside `@media\n  screen and (prefers-reduced-motion: no-preference)`: without JavaScript,\n  in print, with reduced motion or a failed script, the page is final.\n\nWhy: one observer, not one per element; no scroll listener, the browser\nreports what entered; and once, because motion replayed on every return is\nnoise.\n\n```ts\n\u002F** Reveals every [data-reveal] block under `root` once, as it scrolls into\n * view. `prefersReducedMotion()` is the matchMedia check. *\u002F\nexport function startReveal(root: HTMLElement): () => void {\n  if (prefersReducedMotion() || !(\"IntersectionObserver\" in window)) {\n    return () => {};\n  }\n  root.dataset.motion = \"on\";\n  const observer = new IntersectionObserver(\n    (entries) => {\n      \u002F\u002F Siblings arriving together are staggered in reading order (--rv),\n      \u002F\u002F whatever the grid looks like at this width.\n      const arrived = new Map\u003CElement | null, number>();\n      for (const entry of entries) {\n        if (!entry.isIntersecting) continue;\n        const block = entry.target as HTMLElement;\n        const order = arrived.get(block.parentElement) ?? 0;\n        arrived.set(block.parentElement, order + 1);\n        block.style.setProperty(\"--rv\", String(order));\n        block.dataset.shown = \"\";\n        observer.unobserve(block);\n      }\n    },\n    \u002F\u002F Start once a block is a little way above the bottom of the screen.\n    { rootMargin: \"0px 0px -10% 0px\" },\n  );\n  root.querySelectorAll(\"[data-reveal]\").forEach((b) => observer.observe(b));\n  return () => observer.disconnect();\n}\n```\n\n```css\n@media screen and (prefers-reduced-motion: no-preference) {\n  [data-motion] [data-reveal]:not([data-shown]) :is(h2, .lede) { opacity: 0; }\n  [data-motion] [data-reveal][data-shown] :is(h2, .lede) {\n    animation: rise var(--m-enter) var(--m-ease-out) backwards;\n    animation-delay: calc(var(--rv, 0) * 80ms);\n  }\n}\n@keyframes rise { from { opacity: 0; transform: translateY(var(--m-shift)); } }\n```\n\n- Keyframes with only `from` end at the element's own style, so the final\n  state is written once, in the ordinary CSS.\n- `backwards` fill holds the `from` state during the delay; afterwards the\n  element is back to its base style with no animation holding it. Use\n  `both` only for parts replayed later with `getAnimations()`\n  (`motion-drawings`).\n- In React, call it from `useLayoutEffect` on the page root\n  (`motion-stacks`). A framework's own reveal (Motion's `whileInView`)\n  keeps the same rules: once, a margin, reduced motion, nothing hidden\n  without JavaScript.\n\n## 3. Before the first paint\n\nThe flag must be on before the browser paints. Otherwise a block already on\nscreen paints in its final state, disappears, and plays: a flash.\n\n- A client-rendered app: `useLayoutEffect` (or the equivalent) on the root.\n- Server-rendered HTML (Next.js, Astro, a static site) paints before the app\n  runs. Set the flag from an inline script in `\u003Chead>`, with a safety\n  timeout that takes it off if the app never starts:\n\n  ```html\n  \u003Cscript>\n    if (matchMedia(\"(prefers-reduced-motion: no-preference)\").matches &&\n        \"IntersectionObserver\" in window) {\n      document.documentElement.dataset.motion = \"on\";\n      setTimeout(() => {\n        if (!window.__revealStarted) delete document.documentElement.dataset.motion;\n      }, 2500);\n    }\n  \u003C\u002Fscript>\n  ```\n\n  The app sets `window.__revealStarted = true` when its observer starts.\n  Or leave content above the fold without a pre-state, revealing only below.\n- Never hide content in the base CSS (`.fade-in { opacity: 0 }` with no\n  flag): crawlers, failed scripts, reader views, print and reduced motion\n  all get a blank page.\n\n## 4. What moves inside a block\n\n- The children (heading, text, drawing), not the block's frame. In a ruled\n  grid (1px gaps showing the grid's background), a hidden cell shows the\n  rule colour through it as a grey slab: animate what is in the cells.\n- Small distances, 6 to 18px (`--m-shift-sm` to `--m-shift-lg`); longer\n  travel reads as the page sliding. Opacity and transform only, never\n  width, height, margins or `filter: blur()` (`motion-performance`).\n- One entrance per role across the page (every section title the same),\n  and a content-tied one per section body: steps that fill in order, work\n  handed along a line, numbers that count in their own line, a drawing that\n  acts out its card (`motion-drawings`). The same fade-up on every block is\n  the template (`motion-audit`).\n- A headline set as one block per line, so a second line can arrive a beat\n  (300 to 400ms) after the first when the sentence says \"then\".\n- A section title: the rule under it draws from the left (scaleX 0 to 1,\n  `--m-draw`, `--m-ease-in-out`), the eyebrow fades, the heading rises 12px,\n  the lede follows 150ms later. Draw the rule as a pseudo-element of the\n  head, so the base CSS keeps a visible rule.\n\n## 5. Stagger by arrival\n\n- Siblings revealed in the same observer callback get `--rv` 0, 1, 2 in\n  reading order, counted per parent. The delay is `--rv` times 40 to 90ms.\n- It follows the layout at every width: three cards across stagger 0, 1,\n  2; stacked on a phone they arrive one at a time, each at 0. A fixed\n  `nth-child` delay makes a phone's third card wait for nothing.\n- Order inside a block comes from its own index (`--k` on chips or steps),\n  set in the markup. Past about 8 items, 30 to 40ms each; a sequence longer\n  than 2.5 to 3s is a wait (`motion-timing`).\n\n## 6. Edge cases\n\n- Horizontal swipe rows on a phone: make each card its own `data-reveal`.\n  Cards off to the side reveal as they are swiped in, since clipping by a\n  scroll container counts as not intersecting.\n- A page that scrolls inside a container: `root: null` still works, the\n  container's clipping counts.\n- Anchor jumps and keyboard focus bring a block into view, and it reveals.\n  Never make hidden blocks unfocusable to get round this.\n- Very tall blocks: threshold 0 (the default) with the -10% margin; a\n  threshold of 0.5 is never reached by a block over two screens tall.\n- Fast scrolling: blocks passed quickly reveal as they cross; nothing\n  replays when the reader comes back.\n- Content added later (a list loaded after the page): observe new blocks as\n  they mount, or use the list's own entrance (`motion-interface`).\n\n## 7. Scroll-linked motion\n\nScroll-triggered (plays once) is the default. Scroll-linked (progress\nfollows the scroll) is for a reading bar, a figure that assembles as its\nsection passes, a background that shifts gently.\n\n```css\n@supports (animation-timeline: view()) {\n  @media (prefers-reduced-motion: no-preference) {\n    .figure-part {\n      animation: assemble linear both;\n      animation-timeline: view();\n      animation-range: entry 10% cover 40%;\n    }\n  }\n}\n@keyframes assemble { from { opacity: 0; transform: translateY(12px); } }\n```\n\nA reading bar is the same with `animation-timeline: scroll(root)` on a\n`scaleX` bar.\n\n- Where scroll-driven animations are not supported, the element sits in its\n  final state. Do not polyfill it with a scroll listener.\n- Never scroll-link text being read: text that moves while the eye follows\n  it is hard to read and a vestibular trigger (`motion-comfort`).\n- Short ranges, small movement. A scroll-linked effect running the length\n  of the page is wallpaper.\n\n## 8. Parallax\n\n- Rarely: one decorative background layer at most, moving 10 to 20% slower\n  than the page. Never text, never content, never on a phone, never under\n  reduced motion.\n- With a scroll-driven `translateY` on that layer, or not at all. No\n  scroll-event parallax libraries.\n- Layers at several speeds, zooming backgrounds and elements that rotate\n  with the scroll are vestibular triggers: do not.\n\n## 9. Pinned and sticky storytelling\n\nFor a sequence that deserves the screen (how the product works, step by\nstep):\n\n- The visual is `position: sticky`; the steps are ordinary blocks scrolling\n  beside it; one IntersectionObserver marks the current step; the visual\n  changes state when the step changes (a crossfade or one part highlighted,\n  `--m-base`).\n- The page scrolls at the reader's speed: no scroll hijacking, no\n  smooth-scroll libraries (Lenis, Locomotive) by default. They fight the\n  reader's own scrolling, break find in page and anchors, and cost frames.\n- On a phone the sticky visual goes; each step shows its state above it.\n  Under reduced motion every state is shown in order, without transitions.\n\n## 10. The section marker in a nav\n\nA pill under the link of the section on screen, so the reader knows where\nthey are.\n\n- Which section: one IntersectionObserver on the sections with `rootMargin:\n  \"-45% 0px -54% 0px\"`, a thin band just above the middle of the screen.\n  None over the hero or past the last section.\n- Where: the active link's `offsetLeft` and `offsetWidth` inside the\n  positioned nav, applied as `transform: translateX()` and `width`.\n- Coming from no section it fades in where it is; between sections it\n  slides. Otherwise it first slides in from the left edge, which looks like\n  a glitch. A ref holds the previous section: empty means appear in place.\n  Never change `transition` while one runs: it cancels and the pill jumps.\n- A ResizeObserver on the nav measures again when the font arrives or the\n  page is zoomed.\n- `aria-current=\"location\"` on the link, the pill `aria-hidden`, painted\n  behind the words with `z-index: -1` inside the nav's stacking context\n  (a transform or `isolation: isolate` on the nav).\n\n```tsx\nconst previous = useRef\u003Cstring | null>(null);\nconst [mark, setMark] = useState({ x: 0, width: 0, instant: true });\nuseLayoutEffect(() => {\n  const appearing = previous.current === null;\n  previous.current = active;\n  const link = active && nav.current?.querySelector\u003CHTMLElement>(`a[href=\"#${active}\"]`);\n  if (!link) return;\n  const follow = (instant?: boolean) => setMark((m) =>\n    ({ x: link.offsetLeft, width: link.offsetWidth, instant: instant ?? m.instant }));\n  follow(appearing);\n  const resize = new ResizeObserver(() => follow());\n  resize.observe(nav.current!);\n  return () => resize.disconnect();\n}, [active]);\n```\n\n```css\n.nav-mark { opacity: 0; }\n.nav-mark[data-on] { opacity: 1; }\n@media (prefers-reduced-motion: no-preference) {\n  .nav-mark { transition: transform var(--m-panel) var(--m-ease-out),\n    width var(--m-panel) var(--m-ease-out), opacity var(--m-base) ease; }\n  .nav-mark[data-instant] { transition: opacity var(--m-base) ease; }\n}\n```\n\n## 11. Smooth anchor scrolling\n\n- Only without reduced motion, in both places it is switched on: CSS\n  (`scroll-behavior: smooth` inside the no-preference query, on the scroll\n  container when the page scrolls inside one) and script\n  (`scrollIntoView({ behavior: prefersReducedMotion() ? \"auto\" : \"smooth\"\n  })`). A router with a hard-coded `\"smooth\"` ignores the setting.\n- With a sticky header, `scroll-margin-top` on the sections (or\n  `scroll-padding-top` on the scroller): the header's height plus 16 to\n  24px, so the heading lands below it.\n- The back-to-top control follows the same rule (`ui-part-back-to-top`).\n\n## Check it\n\n- `preview` with `motion: true`: the scroll timeline lists each block's\n  entrance and its delays; the list of content still hidden after scrolling\n  through is empty; the reduced-motion pass shows nothing moving, nothing\n  hidden and smooth scrolling off.\n- Load the page with JavaScript off, and print it: everything is there.\n- Scroll fast to the bottom and back up: nothing replays, nothing is blank.\n- Tab through the page from the top: every focused control is in view.\n\n## Avoid\n\nEvery section fading up 40px; reveals on dashboards and forms; an observer\nper element; scroll listeners; content hidden in the base CSS; replaying\non every return; a threshold a tall block never reaches; long travel,\nscale from zero or blur on an entrance; parallax on text; scroll hijacking\nand smooth-scroll libraries; a nav marker that slides in from the left on\nfirst appearance; smooth scrolling that ignores reduced motion.\n","# Motion on scroll\n\nThe generated version: every section fades up 40px as it enters, the same\nway on every page, and the page is blank when the script is late. This is\nhow content arrives in a way that says something, for one observer, never\nhidden from someone who cannot or will not see it move. Principles in\n`motion`, timing in `motion-timing`, cost in `motion-performance`.\n\n## 1. When to reveal on scroll\n\n- At dial 2 and up (`motion`), on pages people read once or now and then:\n  a launch page, a product site, a portfolio, a story, a report.\n- Not on dashboards, application screens, docs, forms, settings, search\n  results, pages used every day, or content scanned for one fact.\n- Reveal a block when its arrival says something: an order (steps in turn),\n  a cause (a line drawn to what it connects), a group arriving together.\n  The rest can just be there; a page where everything fades in has no\n  emphasis left.\n- The first screen never waits for a scroll: it plays its entrance on load,\n  or is simply there.\n\n## 2. The pattern: one observer, reveal once\n\n- Each block that enters carries `data-reveal`. One IntersectionObserver\n  for the page sets `data-shown` on it the first time it comes into view,\n  then stops watching it.\n- The root carries `data-motion`, set before the first paint, only when\n  this can work (the observer exists, motion is welcome).\n- The hidden pre-state exists only under `[data-motion]` inside `@media\n  screen and (prefers-reduced-motion: no-preference)`: without JavaScript,\n  in print, with reduced motion or a failed script, the page is final.\n\nWhy: one observer, not one per element; no scroll listener, the browser\nreports what entered; and once, because motion replayed on every return is\nnoise.\n\n```ts\n\u002F** Reveals every [data-reveal] block under `root` once, as it scrolls into\n * view. `prefersReducedMotion()` is the matchMedia check. *\u002F\nexport function startReveal(root: HTMLElement): () => void {\n  if (prefersReducedMotion() || !(\"IntersectionObserver\" in window)) {\n    return () => {};\n  }\n  root.dataset.motion = \"on\";\n  const observer = new IntersectionObserver(\n    (entries) => {\n      \u002F\u002F Siblings arriving together are staggered in reading order (--rv),\n      \u002F\u002F whatever the grid looks like at this width.\n      const arrived = new Map\u003CElement | null, number>();\n      for (const entry of entries) {\n        if (!entry.isIntersecting) continue;\n        const block = entry.target as HTMLElement;\n        const order = arrived.get(block.parentElement) ?? 0;\n        arrived.set(block.parentElement, order + 1);\n        block.style.setProperty(\"--rv\", String(order));\n        block.dataset.shown = \"\";\n        observer.unobserve(block);\n      }\n    },\n    \u002F\u002F Start once a block is a little way above the bottom of the screen.\n    { rootMargin: \"0px 0px -10% 0px\" },\n  );\n  root.querySelectorAll(\"[data-reveal]\").forEach((b) => observer.observe(b));\n  return () => observer.disconnect();\n}\n```\n\n```css\n@media screen and (prefers-reduced-motion: no-preference) {\n  [data-motion] [data-reveal]:not([data-shown]) :is(h2, .lede) { opacity: 0; }\n  [data-motion] [data-reveal][data-shown] :is(h2, .lede) {\n    animation: rise var(--m-enter) var(--m-ease-out) backwards;\n    animation-delay: calc(var(--rv, 0) * 80ms);\n  }\n}\n@keyframes rise { from { opacity: 0; transform: translateY(var(--m-shift)); } }\n```\n\n- Keyframes with only `from` end at the element's own style, so the final\n  state is written once, in the ordinary CSS.\n- `backwards` fill holds the `from` state during the delay; afterwards the\n  element is back to its base style with no animation holding it. Use\n  `both` only for parts replayed later with `getAnimations()`\n  (`motion-drawings`).\n- In React, call it from `useLayoutEffect` on the page root\n  (`motion-stacks`). A framework's own reveal (Motion's `whileInView`)\n  keeps the same rules: once, a margin, reduced motion, nothing hidden\n  without JavaScript.\n\n## 3. Before the first paint\n\nThe flag must be on before the browser paints. Otherwise a block already on\nscreen paints in its final state, disappears, and plays: a flash.\n\n- A client-rendered app: `useLayoutEffect` (or the equivalent) on the root.\n- Server-rendered HTML (Next.js, Astro, a static site) paints before the app\n  runs. Set the flag from an inline script in `\u003Chead>`, with a safety\n  timeout that takes it off if the app never starts:\n\n  ```html\n  \u003Cscript>\n    if (matchMedia(\"(prefers-reduced-motion: no-preference)\").matches &&\n        \"IntersectionObserver\" in window) {\n      document.documentElement.dataset.motion = \"on\";\n      setTimeout(() => {\n        if (!window.__revealStarted) delete document.documentElement.dataset.motion;\n      }, 2500);\n    }\n  \u003C\u002Fscript>\n  ```\n\n  The app sets `window.__revealStarted = true` when its observer starts.\n  Or leave content above the fold without a pre-state, revealing only below.\n- Never hide content in the base CSS (`.fade-in { opacity: 0 }` with no\n  flag): crawlers, failed scripts, reader views, print and reduced motion\n  all get a blank page.\n\n## 4. What moves inside a block\n\n- The children (heading, text, drawing), not the block's frame. In a ruled\n  grid (1px gaps showing the grid's background), a hidden cell shows the\n  rule colour through it as a grey slab: animate what is in the cells.\n- Small distances, 6 to 18px (`--m-shift-sm` to `--m-shift-lg`); longer\n  travel reads as the page sliding. Opacity and transform only, never\n  width, height, margins or `filter: blur()` (`motion-performance`).\n- One entrance per role across the page (every section title the same),\n  and a content-tied one per section body: steps that fill in order, work\n  handed along a line, numbers that count in their own line, a drawing that\n  acts out its card (`motion-drawings`). The same fade-up on every block is\n  the template (`motion-audit`).\n- A headline set as one block per line, so a second line can arrive a beat\n  (300 to 400ms) after the first when the sentence says \"then\".\n- A section title: the rule under it draws from the left (scaleX 0 to 1,\n  `--m-draw`, `--m-ease-in-out`), the eyebrow fades, the heading rises 12px,\n  the lede follows 150ms later. Draw the rule as a pseudo-element of the\n  head, so the base CSS keeps a visible rule.\n\n## 5. Stagger by arrival\n\n- Siblings revealed in the same observer callback get `--rv` 0, 1, 2 in\n  reading order, counted per parent. The delay is `--rv` times 40 to 90ms.\n- It follows the layout at every width: three cards across stagger 0, 1,\n  2; stacked on a phone they arrive one at a time, each at 0. A fixed\n  `nth-child` delay makes a phone's third card wait for nothing.\n- Order inside a block comes from its own index (`--k` on chips or steps),\n  set in the markup. Past about 8 items, 30 to 40ms each; a sequence longer\n  than 2.5 to 3s is a wait (`motion-timing`).\n\n## 6. Edge cases\n\n- Horizontal swipe rows on a phone: make each card its own `data-reveal`.\n  Cards off to the side reveal as they are swiped in, since clipping by a\n  scroll container counts as not intersecting.\n- A page that scrolls inside a container: `root: null` still works, the\n  container's clipping counts.\n- Anchor jumps and keyboard focus bring a block into view, and it reveals.\n  Never make hidden blocks unfocusable to get round this.\n- Very tall blocks: threshold 0 (the default) with the -10% margin; a\n  threshold of 0.5 is never reached by a block over two screens tall.\n- Fast scrolling: blocks passed quickly reveal as they cross; nothing\n  replays when the reader comes back.\n- Content added later (a list loaded after the page): observe new blocks as\n  they mount, or use the list's own entrance (`motion-interface`).\n\n## 7. Scroll-linked motion\n\nScroll-triggered (plays once) is the default. Scroll-linked (progress\nfollows the scroll) is for a reading bar, a figure that assembles as its\nsection passes, a background that shifts gently.\n\n```css\n@supports (animation-timeline: view()) {\n  @media (prefers-reduced-motion: no-preference) {\n    .figure-part {\n      animation: assemble linear both;\n      animation-timeline: view();\n      animation-range: entry 10% cover 40%;\n    }\n  }\n}\n@keyframes assemble { from { opacity: 0; transform: translateY(12px); } }\n```\n\nA reading bar is the same with `animation-timeline: scroll(root)` on a\n`scaleX` bar.\n\n- Where scroll-driven animations are not supported, the element sits in its\n  final state. Do not polyfill it with a scroll listener.\n- Never scroll-link text being read: text that moves while the eye follows\n  it is hard to read and a vestibular trigger (`motion-comfort`).\n- Short ranges, small movement. A scroll-linked effect running the length\n  of the page is wallpaper.\n\n## 8. Parallax\n\n- Rarely: one decorative background layer at most, moving 10 to 20% slower\n  than the page. Never text, never content, never on a phone, never under\n  reduced motion.\n- With a scroll-driven `translateY` on that layer, or not at all. No\n  scroll-event parallax libraries.\n- Layers at several speeds, zooming backgrounds and elements that rotate\n  with the scroll are vestibular triggers: do not.\n\n## 9. Pinned and sticky storytelling\n\nFor a sequence that deserves the screen (how the product works, step by\nstep):\n\n- The visual is `position: sticky`; the steps are ordinary blocks scrolling\n  beside it; one IntersectionObserver marks the current step; the visual\n  changes state when the step changes (a crossfade or one part highlighted,\n  `--m-base`).\n- The page scrolls at the reader's speed: no scroll hijacking, no\n  smooth-scroll libraries (Lenis, Locomotive) by default. They fight the\n  reader's own scrolling, break find in page and anchors, and cost frames.\n- On a phone the sticky visual goes; each step shows its state above it.\n  Under reduced motion every state is shown in order, without transitions.\n\n## 10. The section marker in a nav\n\nA pill under the link of the section on screen, so the reader knows where\nthey are.\n\n- Which section: one IntersectionObserver on the sections with `rootMargin:\n  \"-45% 0px -54% 0px\"`, a thin band just above the middle of the screen.\n  None over the hero or past the last section.\n- Where: the active link's `offsetLeft` and `offsetWidth` inside the\n  positioned nav, applied as `transform: translateX()` and `width`.\n- Coming from no section it fades in where it is; between sections it\n  slides. Otherwise it first slides in from the left edge, which looks like\n  a glitch. A ref holds the previous section: empty means appear in place.\n  Never change `transition` while one runs: it cancels and the pill jumps.\n- A ResizeObserver on the nav measures again when the font arrives or the\n  page is zoomed.\n- `aria-current=\"location\"` on the link, the pill `aria-hidden`, painted\n  behind the words with `z-index: -1` inside the nav's stacking context\n  (a transform or `isolation: isolate` on the nav).\n\n```tsx\nconst previous = useRef\u003Cstring | null>(null);\nconst [mark, setMark] = useState({ x: 0, width: 0, instant: true });\nuseLayoutEffect(() => {\n  const appearing = previous.current === null;\n  previous.current = active;\n  const link = active && nav.current?.querySelector\u003CHTMLElement>(`a[href=\"#${active}\"]`);\n  if (!link) return;\n  const follow = (instant?: boolean) => setMark((m) =>\n    ({ x: link.offsetLeft, width: link.offsetWidth, instant: instant ?? m.instant }));\n  follow(appearing);\n  const resize = new ResizeObserver(() => follow());\n  resize.observe(nav.current!);\n  return () => resize.disconnect();\n}, [active]);\n```\n\n```css\n.nav-mark { opacity: 0; }\n.nav-mark[data-on] { opacity: 1; }\n@media (prefers-reduced-motion: no-preference) {\n  .nav-mark { transition: transform var(--m-panel) var(--m-ease-out),\n    width var(--m-panel) var(--m-ease-out), opacity var(--m-base) ease; }\n  .nav-mark[data-instant] { transition: opacity var(--m-base) ease; }\n}\n```\n\n## 11. Smooth anchor scrolling\n\n- Only without reduced motion, in both places it is switched on: CSS\n  (`scroll-behavior: smooth` inside the no-preference query, on the scroll\n  container when the page scrolls inside one) and script\n  (`scrollIntoView({ behavior: prefersReducedMotion() ? \"auto\" : \"smooth\"\n  })`). A router with a hard-coded `\"smooth\"` ignores the setting.\n- With a sticky header, `scroll-margin-top` on the sections (or\n  `scroll-padding-top` on the scroller): the header's height plus 16 to\n  24px, so the heading lands below it.\n- The back-to-top control follows the same rule (`ui-part-back-to-top`).\n\n## Check it\n\n- `preview` with `motion: true`: the scroll timeline lists each block's\n  entrance and its delays; the list of content still hidden after scrolling\n  through is empty; the reduced-motion pass shows nothing moving, nothing\n  hidden and smooth scrolling off.\n- Load the page with JavaScript off, and print it: everything is there.\n- Scroll fast to the bottom and back up: nothing replays, nothing is blank.\n- Tab through the page from the top: every focused control is in view.\n\n## Avoid\n\nEvery section fading up 40px; reveals on dashboards and forms; an observer\nper element; scroll listeners; content hidden in the base CSS; replaying\non every return; a threshold a tall block never reaches; long travel,\nscale from zero or blur on an entrance; parallax on text; scroll hijacking\nand smooth-scroll libraries; a nav marker that slides in from the left on\nfirst appearance; smooth scrolling that ignores reduced motion.\n",{"name":5,"description":7},"main","38293fe1b096e6bf3646c7344c38786ed688f464","crates\u002Fenowx-core\u002Fskills\u002Fmotion-reveal",21,"https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli","https:\u002F\u002Fenowx.ai","https:\u002F\u002Fgithub.com\u002Fenowdev","Pretending to be a Developer","2026-10-06 02:05:35",{"source":8,"skillName":5,"commands":44,"prompt":54,"zipAvailable":55},[45,48,51],{"tool":46,"command":47},"npx","npx skills add enowdev\u002Fenowxcli --skill motion-reveal",{"tool":49,"command":50},"bunx","bunx skills add enowdev\u002Fenowxcli --skill motion-reveal",{"tool":52,"command":53},"pnpm","pnpm dlx skills add enowdev\u002Fenowxcli --skill motion-reveal","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fmotion-reveal\u002FSKILL.md",true,[],[58,74,98,114,141,163],{"skillNo":59,"name":60,"nameZh":6,"description":61,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":62,"categories":63,"keywords":69,"publishedAt":29},"01M6D2HQYSQD9N7MTNDT4A7CBM","demo-skill","Project-scope demo skill for tests.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fworkspace\u002F.agents\u002Fskills\u002Fdemo-skill\u002FSKILL.md",[64],{"categoryNo":65,"categoryName":66,"taxonomyType":67,"taxonomyTypeDesc":68},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[70,71,72,73,25],"demo skill","demo","tests","project",{"skillNo":75,"name":76,"nameZh":6,"description":77,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":88,"publishedAt":29},"01M6D2HM0GCPM0TM2HPG9HHR2R","writing","Writing words people read: the specific thing instead of claims, words to drop, sentences, interface copy (buttons, labels, errors, empty states, confirmations), page copy, notifications and emails, alt text and link text, numbers and dates, words that translate well, documentation and voice. Read before writing a page's copy, an interface's text or a document.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fwriting\u002FSKILL.md",[80,83],{"categoryNo":81,"categoryName":82,"taxonomyType":67,"taxonomyTypeDesc":68},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":84,"categoryName":85,"taxonomyType":86,"taxonomyTypeDesc":87},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[76,89,90,91,92,93,25,24,94,95,96,97],"text","emails","empty","drop","document","voice","notifications","documentation","translate",{"skillNo":99,"name":100,"nameZh":6,"description":101,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":102,"categories":103,"keywords":107,"publishedAt":29},"01M6D2HG07YS49NM196Q0R3AZ8","ui","Designing or restyling an interface: direction, layout, type, colour, icons, components, states, responsive behaviour. Read before building a page, a screen or a set of components.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui\u002FSKILL.md",[104],{"categoryNo":105,"categoryName":106,"taxonomyType":86,"taxonomyTypeDesc":87},"01JBSEEDCAT000000000000032","设计师",[100,108,109,110,111,24,112,25,113],"icons","components","SET","responsive","layout","before building",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":126,"publishedAt":29},"01M6D2HB59HZHWG1RYZDV4SJ91","ui-themes","Light and dark themes, and colour that holds in both: tokens with a value per theme, and what each kind of content needs to stay visible (icons, SVG drawings and line art, logos, photos, screenshots, charts, code, maps, borders, shadows, focus, form controls), switching without a flash, and checking both. Read before building a second theme or a theme toggle, and before any drawing, logo or chart on a page that has two themes.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-themes\u002FSKILL.md",[120,123],{"categoryNo":121,"categoryName":122,"taxonomyType":67,"taxonomyTypeDesc":68},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":124,"categoryName":125,"taxonomyType":86,"taxonomyTypeDesc":87},"01JBSEEDCAT000000000000030","开发工程师",[100,127,128,129,130,131,132,133,134,25,113,135,24,136,137,138,108,139,140],"ui themes","switching","photos","tokens","drawing","focus","maps","Art","flash","code","Charts","content","screenshots","logo",{"skillNo":142,"name":143,"nameZh":6,"description":144,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":150,"publishedAt":29},"01M6D2H5R5KRAE524C8QX0T7Q0","ui-structure","Structure before styling: the blueprint written before any interface code (the screen's job, the regions its kind of page needs, their order and why, the layout skeleton in numbers, the components and their states), the anatomy and order of a homepage, a dashboard, a list, a detail page, settings, a form, sign-in, pricing, docs, an article and a checkout, and how to turn the blueprint into code (tokens, layout primitives, components, sections, page). Read first, before building or rebuilding any page or screen.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-structure\u002FSKILL.md",[147,148,149],{"categoryNo":81,"categoryName":82,"taxonomyType":67,"taxonomyTypeDesc":68},{"categoryNo":124,"categoryName":125,"taxonomyType":86,"taxonomyTypeDesc":87},{"categoryNo":84,"categoryName":85,"taxonomyType":86,"taxonomyTypeDesc":87},[151,100,152,109,153,154,136,155,156,157,112,130,158,159,160,25,113,24,161,162],"structure","ui structure","pricing","dashboard","Blueprint","article","anatomy","styling","primitives","list","why","how",{"skillNo":164,"name":165,"nameZh":6,"description":166,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":167,"categories":168,"keywords":176,"publishedAt":29},"01M6D2H0ZZBJG77MBGPZHA6MWD","ui-stack-vue","Building interfaces in Vue 3 and Nuxt: project layout, script setup with typed defineProps, defineEmits and defineModel, attribute fallthrough, slots and scoped slots, template refs, ref, reactive and computed, watch versus watchEffect, composables and VueUse, provide and inject, Pinia, keyed v-for, transitions, Nuxt 4 (app directory, pages, layouts, useFetch and useAsyncData keys, $fetch in handlers, server routes, runtime config, useSeoMeta, NuxtImg, middleware), VeeValidate forms, Reka UI and Nuxt UI, testing, vue-tsc, and the traps. Read when the project uses Vue or Nuxt.","https:\u002F\u002Fgithub.com\u002Fenowdev\u002Fenowxcli\u002Fblob\u002Fmain\u002Fcrates\u002Fenowx-core\u002Fskills\u002Fui-stack-vue\u002FSKILL.md",[169,172,175],{"categoryNo":170,"categoryName":171,"taxonomyType":67,"taxonomyTypeDesc":68},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":173,"categoryName":174,"taxonomyType":67,"taxonomyTypeDesc":68},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":105,"categoryName":106,"taxonomyType":86,"taxonomyTypeDesc":87},[100,177,178,179,180,181,182,183,112,73,184,185,25,186,187,188,189,190,191,192],"vue","stack","ui stack vue","forms","pages","watch","runtime","transitions","fetch","pinia","nuxt","template","server","config","directory","testing"]