Skip to content

Commit a52b2e4

Browse files
authored
feat(site): add load and scroll-reveal animations to landing page (#42)
1 parent 61d6680 commit a52b2e4

4 files changed

Lines changed: 217 additions & 9 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/* Load and scroll-reveal animations for the landing page.
2+
All motion is gated behind prefers-reduced-motion so users who
3+
disable animations see the page in its instant-render state. */
4+
5+
@media (prefers-reduced-motion: no-preference) {
6+
/* ---- keyframes ---- */
7+
@keyframes fade-up {
8+
from {
9+
opacity: 0;
10+
transform: translateY(20px);
11+
}
12+
to {
13+
opacity: 1;
14+
transform: translateY(0);
15+
}
16+
}
17+
18+
@keyframes fade-up-lg {
19+
from {
20+
opacity: 0;
21+
transform: translateY(32px) scale(0.98);
22+
}
23+
to {
24+
opacity: 1;
25+
transform: translateY(0) scale(1);
26+
}
27+
}
28+
29+
@keyframes fade-in {
30+
from {
31+
opacity: 0;
32+
}
33+
to {
34+
opacity: 1;
35+
}
36+
}
37+
38+
@keyframes rule-extend {
39+
from {
40+
transform: scaleX(0) translateY(-0.4em);
41+
}
42+
to {
43+
transform: scaleX(1) translateY(-0.4em);
44+
}
45+
}
46+
47+
/* ---- above-the-fold entrance (pure CSS, no JS) ---- */
48+
49+
.nav {
50+
animation: fade-in 500ms var(--ease-out) both;
51+
}
52+
53+
.page-landing .hero__eyebrow {
54+
animation: fade-up 600ms var(--ease-out) 100ms both;
55+
}
56+
57+
.page-landing .hero__title {
58+
animation: fade-up 700ms var(--ease-out) 200ms both;
59+
}
60+
61+
.page-landing .hero__title-rule {
62+
transform-origin: left center;
63+
animation: rule-extend 500ms var(--ease-out) 500ms both;
64+
}
65+
66+
.page-landing .hero__tag {
67+
animation: fade-up 600ms var(--ease-out) 400ms both;
68+
}
69+
70+
.page-landing .hero__body {
71+
animation: fade-up 600ms var(--ease-out) 520ms both;
72+
}
73+
74+
.page-landing .hero__meta {
75+
animation: fade-up 600ms var(--ease-out) 640ms both;
76+
}
77+
78+
.page-landing .hero::before {
79+
animation: fade-in 800ms var(--ease-out) 800ms both;
80+
}
81+
82+
.page-landing .hero__terminal {
83+
animation: fade-up-lg 800ms var(--ease-out) 400ms both;
84+
}
85+
86+
/* ---- scroll reveals (JS adds .is-visible via IntersectionObserver) ---- */
87+
88+
.reveal {
89+
opacity: 0;
90+
transform: translateY(20px);
91+
transition:
92+
opacity 600ms var(--ease-out),
93+
transform 600ms var(--ease-out);
94+
will-change: opacity, transform;
95+
}
96+
97+
.reveal.is-visible {
98+
opacity: 1;
99+
transform: translateY(0);
100+
}
101+
102+
/* delayed reveal for secondary content within the same section */
103+
.reveal-delay {
104+
opacity: 0;
105+
transform: translateY(16px);
106+
transition:
107+
opacity 600ms var(--ease-out) 80ms,
108+
transform 600ms var(--ease-out) 80ms;
109+
will-change: opacity, transform;
110+
}
111+
112+
.reveal-delay.is-visible {
113+
opacity: 1;
114+
transform: translateY(0);
115+
}
116+
117+
/* ---- cascading children (invariant rows, doc list rows) ---- */
118+
119+
.reveal-stagger > * {
120+
opacity: 0;
121+
transform: translateY(12px);
122+
transition:
123+
opacity 500ms var(--ease-out),
124+
transform 500ms var(--ease-out);
125+
}
126+
127+
.reveal-stagger.is-visible > * {
128+
opacity: 1;
129+
transform: translateY(0);
130+
}
131+
132+
.reveal-stagger.is-visible > :nth-child(1) {
133+
transition-delay: 0ms;
134+
}
135+
.reveal-stagger.is-visible > :nth-child(2) {
136+
transition-delay: 60ms;
137+
}
138+
.reveal-stagger.is-visible > :nth-child(3) {
139+
transition-delay: 120ms;
140+
}
141+
.reveal-stagger.is-visible > :nth-child(4) {
142+
transition-delay: 180ms;
143+
}
144+
.reveal-stagger.is-visible > :nth-child(5) {
145+
transition-delay: 240ms;
146+
}
147+
.reveal-stagger.is-visible > :nth-child(6) {
148+
transition-delay: 300ms;
149+
}
150+
.reveal-stagger.is-visible > :nth-child(7) {
151+
transition-delay: 360ms;
152+
}
153+
.reveal-stagger.is-visible > :nth-child(8) {
154+
transition-delay: 420ms;
155+
}
156+
.reveal-stagger.is-visible > :nth-child(9) {
157+
transition-delay: 480ms;
158+
}
159+
.reveal-stagger.is-visible > :nth-child(10) {
160+
transition-delay: 540ms;
161+
}
162+
.reveal-stagger.is-visible > :nth-child(11) {
163+
transition-delay: 600ms;
164+
}
165+
.reveal-stagger.is-visible > :nth-child(12) {
166+
transition-delay: 660ms;
167+
}
168+
.reveal-stagger.is-visible > :nth-child(n + 13) {
169+
transition-delay: 700ms;
170+
}
171+
}

docs/site/assets/js/site.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// bpetite docs: tiny runtime. Code-block copy + smooth anchor jumps.
1+
// bpetite docs: tiny runtime. Code-block copy, smooth anchor jumps,
2+
// scroll-reveal animations.
23
// Kept pure and dependency-free so the strict CSP (script-src 'self') holds.
34
(() => {
45
"use strict";
@@ -42,6 +43,38 @@
4243
});
4344
};
4445

46+
/**
47+
* Scroll-reveal: observe elements with .reveal, .reveal-delay, or
48+
* .reveal-stagger and add .is-visible when they enter the viewport.
49+
* Respects prefers-reduced-motion: if the user prefers reduced motion,
50+
* skip observation entirely (CSS leaves everything visible by default).
51+
*/
52+
const initScrollReveal = () => {
53+
const prefersReduced = window.matchMedia(
54+
"(prefers-reduced-motion: reduce)"
55+
).matches;
56+
if (prefersReduced) return;
57+
58+
const targets = document.querySelectorAll(
59+
".reveal, .reveal-delay, .reveal-stagger"
60+
);
61+
if (!targets.length) return;
62+
63+
const observer = new IntersectionObserver(
64+
(entries) => {
65+
entries.forEach((entry) => {
66+
if (entry.isIntersecting) {
67+
entry.target.classList.add("is-visible");
68+
observer.unobserve(entry.target);
69+
}
70+
});
71+
},
72+
{ threshold: 0.15 }
73+
);
74+
75+
targets.forEach((el) => observer.observe(el));
76+
};
77+
4578
const onReady = (fn) => {
4679
if (document.readyState === "loading") {
4780
document.addEventListener("DOMContentLoaded", fn, { once: true });
@@ -50,5 +83,8 @@
5083
}
5184
};
5285

53-
onReady(enhanceCodeBlocks);
86+
onReady(() => {
87+
enhanceCodeBlocks();
88+
initScrollReveal();
89+
});
5490
})();

docs/site/templates/base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
/>
2626
<link rel="stylesheet" href="{{ base }}/assets/css/tokens.css" />
2727
<link rel="stylesheet" href="{{ base }}/assets/css/base.css" />
28+
<link rel="stylesheet" href="{{ base }}/assets/css/animations.css" />
2829
{% block head %}{% endblock %}
2930
</head>
3031
<body class="{% block body_class %}{% endblock %}">

docs/site/templates/landing.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,13 @@ <h1 class="hero__title">
6565
</section>
6666

6767
<section class="section" id="overview">
68-
<div class="section__head">
68+
<div class="section__head reveal">
6969
<span class="section__kicker">01</span>
7070
<h2 class="section__title">what it is</h2>
7171
</div>
7272
<div class="section__grid">
73-
<div class="section__prose">{{ overview_html | safe }}</div>
74-
<dl class="invariants" aria-label="invariants">
73+
<div class="section__prose reveal-delay">{{ overview_html | safe }}</div>
74+
<dl class="invariants reveal-stagger" aria-label="invariants">
7575
{% for item in invariants %}
7676
<div class="invariants__row">
7777
<dt class="invariants__key">{{ item.key }}</dt>
@@ -83,23 +83,23 @@ <h2 class="section__title">what it is</h2>
8383
</section>
8484

8585
<section class="section section--why" id="why">
86-
<div class="section__head">
86+
<div class="section__head reveal">
8787
<span class="section__kicker">02</span>
8888
<h2 class="section__title">why I built it</h2>
8989
</div>
90-
<div class="section__prose section__prose--wide">{{ why_html | safe }}</div>
90+
<div class="section__prose section__prose--wide reveal-delay">{{ why_html | safe }}</div>
9191
</section>
9292

9393
<section class="section section--docs" id="docs">
94-
<div class="section__head">
94+
<div class="section__head reveal">
9595
<span class="section__kicker">03</span>
9696
<h2 class="section__title">documentation</h2>
9797
<p class="section__note">
9898
auto-discovered from <span class="mono">docs/</span> at build time. more
9999
will land as the codebase grows.
100100
</p>
101101
</div>
102-
<ol class="doclist">
102+
<ol class="doclist reveal-stagger">
103103
{% for doc in docs %}
104104
<li class="doclist__row">
105105
<a class="doclist__link" href="{{ base }}/docs/{{ doc.slug }}.html">

0 commit comments

Comments
 (0)