Quickly navigate through the dashboard with your keyboard using the command menu.
A headline that gives in to gravity — click and every word drops, tumbles and piles up; drag words to toss them, click the empty floor to restack the sentence.
Falling Words is a headline that obeys gravity on demand. Click it and every word turns into a rigid physical body — the sentence collapses, tumbles and piles up on the floor; you can drag words to toss them around, then click the empty floor to sweep the sentence back into place. Reach for it on a hero or a playful "about" section when you want a line of copy to feel like a real, pokeable object.
This gem runs on matter-js, a 2D physics engine loaded from a CDN — there's no build step and no bundler.
index.html, assets/style.css, and assets/script.js into one folder.assets/byq-brand.css alongside them — it supplies the BYQ brand tokens
(colors, fonts, radii) the gem references.index.html in any modern browser.The <script src="…matter.min.js"> tag in index.html pulls the engine in before
script.js runs. This is a pure-text effect, so the assets/medias/ folder is
empty — there are no images to copy.
The whole gem is one namespaced <section>. The markup is just a heading; the
script splits it into words at runtime.
<section class="byq-gem byq-gem--falling-words-01">
<div
class="byq-gem--falling-words-01__stage"
role="button"
tabindex="0"
aria-pressed="false"
aria-label="Toggle gravity on the headline"
>
<h2 class="byq-gem--falling-words-01__headline">
Every word up here is
<em class="byq-gem--falling-words-01__hl byq-gem--falling-words-01__hl--lime">heavier</em>
than it looks — one click and the whole sentence gives in to
<em class="byq-gem--falling-words-01__hl">gravity</em>
</h2>
</div>
</section>The __stage is the physics world — a fixed-size box that clips the pile. It's a
role="button" with tabindex="0" and aria-pressed, so it's keyboard-operable
and announces its on/off state. The two <em> accents (__hl and __hl--lime)
are the only styled words; the script preserves their classes when it splits the
line.
Every selector starts with .byq-gem--falling-words-01 so the gem can't collide
with anything else on the page. The stage is a clipped box with a hairline floor.
.byq-gem--falling-words-01__stage {
position: relative;
width: min(58rem, 92vw);
height: min(26rem, 70dvh);
overflow: hidden;
border-bottom: 1px solid var(--byq-depth);
}Before the script runs, the headline is just static, absolutely-positioned text.
When physics goes live, the stage gets .is-live and each word switches to
absolute positioning driven entirely by transform from the stage's origin:
.byq-gem--falling-words-01__stage.is-live .byq-gem--falling-words-01__headline {
position: static;
}
.byq-gem--falling-words-01__stage.is-live .word {
position: absolute;
top: 0;
left: 0;
cursor: grab;
}The restack animation is pure CSS. When the engine is torn down, .is-restacking
adds a transition, and the script tweens each word's transform back home:
.byq-gem--falling-words-01__stage.is-restacking .word {
transition: transform 0.62s cubic-bezier(0.22, 1, 0.36, 1);
}
@media (prefers-reduced-motion: reduce) {
.byq-gem--falling-words-01__stage.is-restacking .word {
transition-duration: 0.001ms;
}
}The accents use var(--byq-accent-solar) and var(--byq-accent-lime); the reduced-
motion block collapses the restack to an instant snap.
The script runs matter-js headless — there's no canvas renderer. The engine
simulates plain rectangles, and every tick the script copies each body's position
onto the matching word <span> with a CSS transform.
window.const root = document.querySelector(".byq-gem--falling-words-01");
if (!root || typeof Matter === "undefined") return;
const { Engine, Bodies, Body, Composite, Runner, Events, Mouse, MouseConstraint, Sleeping } = Matter;<span class="word"> — carrying the <em> accent class
through so colored words stay colored. Then flag the stage is-ready.span.className = accent ? `word ${accent}` : "word";
span.textContent = token;Measure the ink, not the box. A line box adds dead air above and below the
glyphs, so bodies sized to it would stack with gaps. Instead each word is measured
on a canvas with measureText, using actualBoundingBoxAscent/Descent to find
the real letterform height and its center (cx, cy) — the rotation pivot.
Drop: build the world. drop() creates an engine with sleeping enabled and
gravity.y = 1.4, then makes one chamfered rectangle per word — low restitution
(0.18) and high friction so words feel heavy and grippy rather than bouncy.
const engine = Engine.create({ enableSleeping: true });
engine.gravity.y = 1.4;setStatic), then join it on a timer.
The release order sorts by vertical position so the bottom line lands first —
a falling word only ever crosses words that are already solid.setTimeout(() => {
released[idx] = true;
Composite.add(engine.world, bodies[idx]);
Body.setVelocity(bodies[idx], { x: (Math.random() - 0.5) * 1.2, y: 0 });
}, k * 24 + Math.random() * 60);afterUpdate listener writes every released
body's position and angle onto its span as a translate3d(...) rotate(...).
Walls (a floor plus tall side walls and a ceiling) keep a hard toss on-stage, and
a MouseConstraint lets you grab and fling words.Events.on(engine, "afterUpdate", () => {
bodies.forEach((body, i) => {
if (!released[i]) return;
place(words[i], spots[i], body.position.x, body.position.y, body.angle);
});
});Restack: hand off to CSS. restack() stops the runner, unbinds matter's mouse
listeners by hand, clears the engine, then adds .is-restacking and — on the next
frame — tweens each word home with a staggered transitionDelay of i * 14ms.
Click detection and state machine. A pointerup only counts as a click if the
pointer moved less than 8px and didn't grab a word, so dragging never restacks by
accident. The stage cycles idle → live → restacking → idle; Enter/Space
trigger the same toggle, and prefers-reduced-motion skips the drop entirely.
<h2> text; the split runs on whatever you write. Wrap
words in <em class="…__hl"> (solar) or add …__hl--lime for the second accent.engine.gravity.y (default 1.4) for a harder fall, or
lower it for a floaty drift.restitution (bounce), friction, and frictionAir on the
body options — lower friction makes the pile slide and spread more.k * 24 + Math.random() * 60 timing controls how quickly
words let go; bigger numbers = a slower, more deliberate collapse.0.62s transition on .is-restacking .word and the
i * 14 stagger to make the sentence reassemble faster or in a tighter wave.--byq-accent-solar / --byq-accent-lime on the __hl
rules, or --byq-depth on the floor line, to re-skin it.