A still — with scripts on, it runs right here The demo itself — scroll inside the frame
A still — with scripts on, it runs right here The demo itself — scroll inside the frame
Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.
Everything in this panel, including the contents behind each URL, in one Markdown file — Large, so it goes as a file (copy is for 32 KB and under)
English underneath — English subtitle under a title Under a large Japanese heading, draw a rule and bring in an English subtitle. Put a 3px rule of the heading’s width directly beneath it and draw it with transform: scaleX(0 → 1) from the left edge. Set the English below in italic serif (Georgia), tightening its letter-spacing from 0.6em to 0.14em while its opacity goes from 0 to 1. In a 340vh pinned section run the rule from 12% to 42% progress and the English from 30% to 75%, both with ease-out 1 − (1 − t)³. Keep the English supporting, smaller than the heading and in var(--muted). Never wrap, and size with clamp so it fits at 375px. With reduced motion stop with the rule drawn and the English shown. Implement it with CSS and JavaScript only, without any library. Do not drop: - Draw the rule with scaleX, never by animating width - Keep the English smaller and quieter than the heading so it never leads - Never wrap; fit at 375px with clamp - Stop with the rule and the English shown under reduced motion A working example (one self-contained HTML file): https://design-vault.pages.dev/designs/motion-english-underline/demo.html
/designs/motion-english-underline/demo.html — One self-contained HTML file. CSS and plain JavaScript only; it fetches nothing.
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>59 下に英語 — English subtitle under a title</title>
<style>
/* dv-motion-base:start */
:root {
--ground: #FFFFFF;
--surface: #F3F5F8;
--ink: #14181F;
--muted: #5A6472;
--accent: #1F5FA8;
--rule: #D8DDE4;
--accent-soft: color-mix(in srgb, var(--accent) 12%, var(--ground));
--accent-mid: color-mix(in srgb, var(--accent) 45%, var(--ground));
--shade: color-mix(in srgb, var(--ink) 14%, transparent);
--font: "Zen Kaku Gothic New", "Hiragino Sans", "Noto Sans JP", "Yu Gothic", system-ui, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--t-label: 13px;
--t-body: 16px;
--t-lead: 20px;
--t-h2: 32px;
--t-h1: clamp(40px, 7vw, 72px);
--s1: 4px;
--s2: 8px;
--s3: 16px;
--s4: 24px;
--s5: 40px;
--s6: 64px;
--s7: 104px;
--radius: 12px;
--ease: cubic-bezier(.2, .7, .2, 1);
--dur: .7s;
--measure: 1080px;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--ground);
color: var(--ink);
font-family: var(--font);
font-size: var(--t-body);
line-height: 1.7;
font-feature-settings: "palt";
overflow-x: hidden;
}
h1, h2, h3, p, ul, ol, figure { margin: 0; padding: 0; }
ul, ol { list-style: none; }
.dv-wrap { width: min(100% - 2 * var(--s4), var(--measure)); margin-inline: auto; }
.dv-label {
min-height: 72vh;
display: grid;
align-content: center;
gap: var(--s3);
padding-block: var(--s6);
border-bottom: 1px solid var(--rule);
}
.dv-num { font-family: var(--mono); font-size: var(--t-lead); font-weight: 700; color: var(--accent); letter-spacing: .04em; }
.dv-name { font-size: var(--t-h1); font-weight: 700; line-height: 1.15; letter-spacing: .01em; }
.dv-ai { font-family: var(--mono); font-size: var(--t-lead); color: var(--muted); }
.dv-desc { font-size: var(--t-lead); color: var(--ink); }
.dv-hint { font-size: var(--t-label); color: var(--muted); letter-spacing: .12em; margin-top: var(--s4); }
.dv-stage { padding-block: var(--s7); }
.dv-h2 { font-size: var(--t-h2); font-weight: 700; line-height: 1.3; }
.dv-lead { font-size: var(--t-lead); color: var(--muted); }
.dv-eyebrow { font-family: var(--mono); font-size: var(--t-label); color: var(--accent); letter-spacing: .12em; text-transform: uppercase; }
.dv-card { background: var(--surface); border: 1px solid var(--rule); border-radius: var(--radius); padding: var(--s4); }
.dv-ph {
border-radius: var(--radius);
background: linear-gradient(135deg, var(--accent-soft), var(--accent-mid) 60%, var(--accent));
aspect-ratio: 16 / 9;
}
.dv-ph--alt { background: linear-gradient(200deg, var(--surface), var(--accent-soft) 55%, var(--accent-mid)); }
.dv-ph--ink { background: linear-gradient(160deg, var(--ink), color-mix(in srgb, var(--ink) 55%, var(--accent))); }
.dv-end {
min-height: 60vh;
display: grid;
place-items: center;
border-top: 1px solid var(--rule);
color: var(--muted);
font-family: var(--mono);
font-size: var(--t-label);
letter-spacing: .12em;
}
/* dv-motion-base:end */
/* ---- この演出だけの CSS。色は var(--…) か color-mix() のみ ---- */
.stack { display: grid; gap: var(--s6); }
.eu-zone { height: 340vh; }
.eu-stick { position: sticky; top: 0; height: 100vh; display: grid; place-content: center; gap: var(--s4); overflow: clip; }
@media (prefers-reduced-motion: reduce) {
.eu-zone { height: auto; }
.eu-stick { position: static; height: auto; padding-block: var(--s6); }
}
.eu-wrap { width: max-content; max-width: 100%; margin-inline: auto; display: grid; gap: var(--s3); justify-items: center; }
.eu-title { font-size: clamp(44px, 12vw, 150px); font-weight: 700; line-height: 1.15; letter-spacing: .06em; }
.eu-rule { width: 100%; height: 3px; background: var(--ink); transform-origin: left center; transform: scaleX(var(--r, 1)); }
.eu-en { font-family: Georgia, "Times New Roman", serif; font-style: italic; font-size: clamp(14px, 2.6vw, 30px); color: var(--muted); letter-spacing: var(--ls, .14em); opacity: var(--o, 1); white-space: nowrap; width: 0; display: flex; justify-content: center; }
</style>
</head>
<body data-motion-number="59">
<header class="dv-label">
<div class="dv-wrap">
<p class="dv-num">59</p>
<h1 class="dv-name">下に英語</h1>
<p class="dv-ai">English subtitle under a title</p>
<p class="dv-desc">線が引かれ、下に英語が字間を詰めて現れる</p>
<p class="dv-hint">SCROLL ↓</p>
</div>
</header>
<main class="dv-stage">
<div class="dv-wrap stack">
<section class="dv-card">
<p class="dv-eyebrow">Section 1</p>
<h2 class="dv-h2">見出しが入ります</h2>
<p class="dv-lead">本文のダミーです。スクロールすると、見出しの下に線が引かれ、英語の副題が広い字間から詰まりながら現れます。</p>
</section>
</div>
<section class="eu-zone" data-progress="pin">
<div class="eu-stick">
<div class="eu-wrap">
<h2 class="eu-title">下に英語</h2>
<div class="eu-rule" aria-hidden="true"></div>
<p class="eu-en" lang="en">English language placement</p>
</div>
</div>
</section>
<div class="dv-wrap stack">
<section class="dv-card">
<p class="dv-eyebrow">Section 2</p>
<h2 class="dv-h2">見出しが入ります</h2>
<p class="dv-lead">本文のダミーです。英語は、日本語の見出しの意味を補うだけで、主役にはしません。</p>
</section>
</div>
</main>
<footer class="dv-end">END — 59 下に英語</footer>
<script>
/* dv-motion-helpers:start */
const dvReduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
document.documentElement.classList.add('js');
/**
* 画面に入った要素へ .is-in を付ける。
* data-reveal="repeat" の要素は画面を出たら外す(何度でも再生)。
*/
function dvReveal(selector = '[data-reveal]', { threshold = 0.2, rootMargin = '0px 0px -8% 0px' } = {}) {
const els = [...document.querySelectorAll(selector)];
if (dvReduce || !('IntersectionObserver' in window)) {
els.forEach((el) => el.classList.add('is-in'));
return;
}
const io = new IntersectionObserver((entries) => {
for (const e of entries) {
if (e.isIntersecting) {
e.target.classList.add('is-in');
if (e.target.dataset.reveal !== 'repeat') io.unobserve(e.target);
} else if (e.target.dataset.reveal === 'repeat') {
e.target.classList.remove('is-in');
}
}
}, { threshold, rootMargin });
els.forEach((el) => io.observe(el));
}
/**
* スクロール量を 0〜1 にして、各 [data-progress] 要素の CSS 変数 --p に書く。
* data-progress="through" : 要素の上端が画面の下端に来たとき 0、下端が画面の上端を抜けたとき 1
* data-progress="pin" : 要素の上端が画面の上端に来たとき 0、下端が画面の下端に来たとき 1
* (中に position: sticky の箱を持つ、背の高い区画用)
* data-progress="page" : ページ全体のスクロール量
* onUpdate(el, p) を渡すと、値が変わるたびに呼ばれる。
*/
function dvProgress(onUpdate) {
const els = [...document.querySelectorAll('[data-progress]')];
const clamp = (n) => Math.min(1, Math.max(0, n));
let queued = false;
const measure = () => {
queued = false;
const vh = window.innerHeight;
for (const el of els) {
const r = el.getBoundingClientRect();
let p;
if (el.dataset.progress === 'pin') p = r.height > vh ? -r.top / (r.height - vh) : 0;
else if (el.dataset.progress === 'page') {
const max = document.documentElement.scrollHeight - vh;
p = max > 0 ? window.scrollY / max : 0;
} else p = (vh - r.top) / (vh + r.height);
p = clamp(p);
el.style.setProperty('--p', p.toFixed(4));
if (onUpdate) onUpdate(el, p);
}
};
const request = () => {
if (!queued) { queued = true; requestAnimationFrame(measure); }
};
addEventListener('scroll', request, { passive: true });
addEventListener('resize', request);
measure();
}
/* dv-motion-helpers:end */
/* ---- この演出だけの JS ---- */
const stage = document.querySelector('.eu-wrap');
const clamp = (n) => Math.min(1, Math.max(0, n));
const ease = (t) => 1 - Math.pow(1 - t, 3);
function place(p) {
const rule = ease(clamp((p - 0.12) / 0.3));
const en = ease(clamp((p - 0.3) / 0.45));
stage.style.setProperty('--r', rule.toFixed(3));
stage.style.setProperty('--o', en.toFixed(3));
stage.style.setProperty('--ls', (0.6 - 0.46 * en).toFixed(3) + 'em'); // 0.6em → 0.14em
}
if (!dvReduce) dvProgress((el, p) => place(p));
</script>
</body>
</html>