
Blinds over images
Images, Scroll
NewLifetime
An endless row tilted by eight degrees, with its pictures kept upright, that slides sideways as you scroll the page and can be dragged and thrown with inertia.
A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN.
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>La galleria in diagonale</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/standard.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<svg class="disegni" aria-hidden="true" focusable="false" width="0" height="0">
<defs>
<symbol id="d-girasole" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#F3E9D2" />
<g fill="#2F5DA8"><circle cx="0" cy="0" r="46" /><circle cx="200" cy="0" r="46" /><circle cx="0" cy="200" r="46" /><circle cx="200" cy="200" r="46" /></g>
<g fill="#2F5DA8" transform="translate(100 100)">
<ellipse rx="12" ry="40" cy="-38" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(45)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(90)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(135)" />
<ellipse rx="12" ry="40" cy="-38" transform="rotate(180)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(225)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(270)" /><ellipse rx="12" ry="40" cy="-38" transform="rotate(315)" />
</g>
<circle cx="100" cy="100" r="22" fill="#E2A93B" />
</symbol>
<symbol id="d-onda" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#E7EEF3" />
<g fill="none" stroke-width="14" stroke-linecap="round">
<path d="M-10 40 Q15 16 40 40 T90 40 T140 40 T190 40 T240 40" stroke="#1E4F8C" />
<path d="M-10 80 Q15 56 40 80 T90 80 T140 80 T190 80 T240 80" stroke="#3D7CC2" />
<path d="M-10 120 Q15 96 40 120 T90 120 T140 120 T190 120 T240 120" stroke="#1E4F8C" />
<path d="M-10 160 Q15 136 40 160 T90 160 T140 160 T190 160 T240 160" stroke="#3D7CC2" />
</g>
</symbol>
<symbol id="d-stella" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#F6E7D8" />
<path d="M100 18 L122 78 L182 100 L122 122 L100 182 L78 122 L18 100 L78 78 Z" fill="#B5452B" />
<path d="M100 42 L141 59 L158 100 L141 141 L100 158 L59 141 L42 100 L59 59 Z" fill="none" stroke="#B5452B" stroke-width="6" />
<rect x="80" y="80" width="40" height="40" fill="#F2C14E" transform="rotate(45 100 100)" />
</symbol>
<symbol id="d-foglia" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#EAF1E3" />
<path d="M100 190 C100 140 100 80 100 14" stroke="#2F5C3C" stroke-width="5" fill="none" />
<g fill="#3F7A4E"><ellipse cx="72" cy="60" rx="30" ry="13" transform="rotate(-30 72 60)" /><ellipse cx="128" cy="96" rx="30" ry="13" transform="rotate(30 128 96)" /><ellipse cx="72" cy="132" rx="30" ry="13" transform="rotate(-30 72 132)" /></g>
<g fill="#6FA36A"><ellipse cx="128" cy="40" rx="24" ry="10" transform="rotate(30 128 40)" /><ellipse cx="72" cy="96" rx="24" ry="10" transform="rotate(-30 72 96)" /><ellipse cx="128" cy="150" rx="24" ry="10" transform="rotate(30 128 150)" /></g>
</symbol>
<symbol id="d-rombo" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#F4EEE6" />
<path d="M100 8 L192 100 L100 192 L8 100 Z" fill="#2A3F6B" />
<path d="M100 38 L162 100 L100 162 L38 100 Z" fill="#E0A43B" />
<path d="M100 68 L132 100 L100 132 L68 100 Z" fill="#B94A36" />
</symbol>
<symbol id="d-nodo" viewBox="0 0 200 200">
<rect width="200" height="200" fill="#EDE3F0" />
<g fill="none" stroke="#5B3A7A" stroke-width="10">
<circle cx="76" cy="76" r="44" /><circle cx="124" cy="76" r="44" /><circle cx="76" cy="124" r="44" /><circle cx="124" cy="124" r="44" />
</g>
</symbol>
<symbol id="freccia" viewBox="0 0 24 24"><path d="M4 12h15M13 6l6 6-6 6" /></symbol>
</defs>
</svg>
<header class="apertura">
<p class="apertura__sopra">Una bottega di maiolica</p>
<h1 class="apertura__titolo">Sei decori,<br>in diagonale</h1>
<p class="apertura__testo">Scorri la pagina: la fila sale di traverso. Puoi anche trascinarla, o usare le frecce.</p>
</header>
<!-- la fila: inclinata di -8 gradi, ogni quadro raddrizzato di +8; le copie prima e dopo la fanno infinita -->
<section class="galleria" data-galleria aria-label="Decori">
<div class="galleria__inclinato">
<div class="galleria__binario" data-binario>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-girasole" /></svg></div>
<figcaption><span class="vagone__nome">Girasole</span><span class="vagone__nota">Blu cobalto e giallo zafferano</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-onda" /></svg></div>
<figcaption><span class="vagone__nome">Onda</span><span class="vagone__nota">Due azzurri che si inseguono</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-stella" /></svg></div>
<figcaption><span class="vagone__nome">Stella</span><span class="vagone__nota">Rosso mattone su crema</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-foglia" /></svg></div>
<figcaption><span class="vagone__nome">Foglia</span><span class="vagone__nota">Verde salvia e verde bosco</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-rombo" /></svg></div>
<figcaption><span class="vagone__nome">Rombo</span><span class="vagone__nota">Blu notte, ocra e ruggine</span></figcaption>
</figure>
<figure class="vagone">
<div class="vagone__quadro"><svg viewBox="0 0 200 200" preserveAspectRatio="xMidYMid slice" aria-hidden="true"><use href="#d-nodo" /></svg></div>
<figcaption><span class="vagone__nome">Nodo</span><span class="vagone__nota">Viola prugna, a mano libera</span></figcaption>
</figure>
</div>
</div>
<div class="galleria__frecce">
<button class="freccia freccia--indietro" type="button" data-indietro aria-label="Decoro precedente"><svg aria-hidden="true"><use href="#freccia" /></svg></button>
<button class="freccia" type="button" data-avanti aria-label="Decoro successivo"><svg aria-hidden="true"><use href="#freccia" /></svg></button>
</div>
</section>
<section class="chiusura">
<p>Ogni decoro nasce dipinto a mano su una piastrella cruda, poi va in forno una seconda volta. Nessuno è uguale al vicino.</p>
</section>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>/* La galleria in diagonale */
:root {
--carta: #F6F2EA;
--inchiostro: #1F2440;
--tenue: #6A6E83;
--inclinazione: -8deg; /* quanto sale la fila; i quadri girano al contrario */
--largo: 52vw; /* un quadro */
--alto: 33.7vw;
--spazio: 2.6vw;
--margine: clamp(18px, 4.5vw, 64px);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Mona Sans Variable', system-ui, sans-serif;
color: var(--inchiostro);
background: var(--carta);
}
.disegni {
position: absolute;
width: 0;
height: 0;
}
button {
font: inherit;
}
.apertura {
display: grid;
align-content: end;
gap: 18px;
min-height: 70vh;
padding: var(--margine);
}
.apertura__sopra {
margin: 0;
color: var(--tenue);
font-size: 0.95rem;
font-weight: 600;
}
.apertura__titolo {
margin: 0;
font-size: clamp(3rem, 1rem + 8vw, 10rem);
font-weight: 700;
font-stretch: 112%;
line-height: 0.88;
letter-spacing: -0.045em;
}
.apertura__testo {
max-width: 34ch;
margin: 0;
font-size: clamp(1.05rem, 0.95rem + 0.4vw, 1.35rem);
line-height: 1.4;
}
/* ------------------------------------------------------------------ la fila inclinata */
.galleria {
position: relative;
height: calc(var(--alto) + 30vw);
min-height: 420px;
overflow: hidden;
cursor: grab;
touch-action: pan-y;
user-select: none;
-webkit-user-select: none;
}
.galleria.presa {
cursor: grabbing;
}
.galleria__inclinato {
position: absolute;
left: 50%;
top: 46%;
transform: translate(-50%, -50%) rotate(var(--inclinazione));
}
.galleria__binario {
display: flex;
gap: var(--spazio);
width: max-content;
will-change: transform;
}
.vagone {
flex: none;
width: var(--largo);
margin: 0;
transform: rotate(calc(var(--inclinazione) * -1));
}
.vagone__quadro {
height: var(--alto);
overflow: hidden;
background: #ddd;
}
.vagone__quadro svg {
display: block;
width: 100%;
height: 100%;
pointer-events: none;
}
.vagone figcaption {
display: flex;
justify-content: space-between;
gap: 16px;
padding-top: 12px;
font-size: clamp(0.9rem, 0.8rem + 0.35vw, 1.1rem);
}
.vagone__nome {
font-weight: 650;
}
.vagone__nota {
color: var(--tenue);
text-align: right;
}
.galleria__frecce {
position: absolute;
right: var(--margine);
bottom: var(--margine);
display: flex;
gap: 10px;
}
.freccia {
display: grid;
place-items: center;
width: 54px;
height: 54px;
border: 0;
border-radius: 50%;
background: var(--inchiostro);
color: var(--carta);
cursor: pointer;
transition: transform 0.16s cubic-bezier(0.23, 1, 0.32, 1);
}
.freccia:active {
transform: scale(0.94);
}
.freccia:focus-visible {
outline: 2px solid var(--inchiostro);
outline-offset: 3px;
}
.freccia svg {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.freccia--indietro svg {
transform: rotate(180deg);
}
.chiusura {
min-height: 70vh;
padding: var(--margine);
display: grid;
align-content: center;
}
.chiusura p {
max-width: 30ch;
margin: 0;
font-size: clamp(1.4rem, 1rem + 1.6vw, 2.6rem);
font-weight: 560;
line-height: 1.15;
letter-spacing: -0.02em;
}
@media (max-width: 700px) {
:root {
--inclinazione: -4deg;
--largo: 82vw;
--alto: 85vw;
--spazio: 16px;
}
.galleria {
height: calc(var(--alto) + 60vw);
}
.galleria__frecce {
left: 0;
right: 0;
justify-content: center;
}
.vagone figcaption {
flex-direction: column;
gap: 2px;
}
.vagone__nota {
text-align: left;
}
}/* La galleria in diagonale
La fila è inclinata di -8 gradi e ogni quadro è raddrizzato di +8: le immagini salgono di traverso ma
restano dritte. Il binario si sposta di un pixel di lato per ogni pixel di pagina scorsa; si può anche
trascinare (col dito o col mouse) e lanciare, e allora scivola con l'inerzia. Copie prima e dopo, e
una posizione che si avvolge: la fila non finisce mai. */
/* ---- valori da cambiare ---- */
const ACCOPPIATA = 1; /* pixel di lato per ogni pixel di pagina scorsa (0 = solo trascinamento) */
const TRASCINA = 2.5; /* quanto corre la fila rispetto al dito */
const LANCIO = 1200; /* forza del lancio quando lasci andare */
const ATTRITO = 0.94; /* quanto frena il lancio a ogni sessantesimo di secondo */
const DERIVA = 18; /* pixel al secondo di movimento da sola quando nessuno la tocca (0 = ferma) */
const RIDOTTO = matchMedia('(prefers-reduced-motion: reduce)').matches;
const galleria = document.querySelector('[data-galleria]');
const binario = document.querySelector('[data-binario]');
const originali = [...binario.children];
/* tre serie: copie prima, originali, copie dopo; la posizione si avvolge su una serie */
originali.forEach((v) => { const c = v.cloneNode(true); c.setAttribute('aria-hidden', 'true'); binario.appendChild(c); });
originali.slice().reverse().forEach((v) => { const c = v.cloneNode(true); c.setAttribute('aria-hidden', 'true'); binario.prepend(c); });
let passo = 0;
let giro = 0;
let avvolgi = (x) => x;
function misura() {
const a = binario.children[0].getBoundingClientRect();
const stile = getComputedStyle(binario);
passo = binario.children[0].offsetWidth + (parseFloat(stile.columnGap) || 0);
giro = passo * originali.length;
avvolgi = gsap.utils.wrap(-giro / 2, giro / 2);
return a;
}
misura();
addEventListener('resize', misura);
let bersaglio = 0;
let corrente = 0;
let momento = 0;
let preso = null;
let sopra = false;
const imposta = gsap.quickSetter(binario, 'x', 'px');
/* la pagina scorre: quanto la sezione è vicina allo schermo, tanto la fila si sposta di lato */
const inVista = () => {
const r = galleria.getBoundingClientRect();
return r.bottom > -innerHeight * 0.12 && r.top < innerHeight * 1.12;
};
let scorsoPrima = scrollY;
gsap.ticker.add((t, dtMs) => {
const dt = Math.min(0.1, dtMs / 1000);
const scorso = scrollY - scorsoPrima;
scorsoPrima = scrollY;
if (!RIDOTTO && inVista()) bersaglio -= scorso * ACCOPPIATA;
if (!preso && !RIDOTTO) {
if (Math.abs(momento) > 1) {
bersaglio += momento * dt;
momento *= Math.pow(ATTRITO, dt * 60);
} else if (!sopra && inVista()) {
bersaglio -= DERIVA * dt;
}
}
const k = RIDOTTO ? 1 : preso ? 0.08 : 0.12;
corrente += (bersaglio - corrente) * (1 - Math.pow(1 - k, dt * 120));
imposta(avvolgi(corrente));
});
/* ---------------------------------------------------------------- trascinare e lanciare */
galleria.addEventListener('pointerdown', (e) => {
if (e.button !== 0 || e.target.closest('button')) return;
preso = { x: e.clientX, y: e.clientY, bloccato: null, campioni: [], id: e.pointerId };
momento = 0;
});
addEventListener('pointermove', (e) => {
if (!preso || e.pointerId !== preso.id) return;
const dx = e.clientX - preso.x;
const dy = e.clientY - preso.y;
if (preso.bloccato === null) {
if (Math.hypot(dx, dy) < 5) return;
/* decide l'asse: di lato è nostro, in verticale è della pagina */
preso.bloccato = Math.abs(dx) > Math.abs(dy) ? 'x' : 'y';
if (preso.bloccato === 'x') {
galleria.classList.add('presa');
galleria.setPointerCapture(e.pointerId);
}
}
if (preso.bloccato !== 'x') return;
const passoDito = e.clientX - (preso.ultimoX ?? preso.x);
preso.ultimoX = e.clientX;
bersaglio += passoDito * (RIDOTTO ? 1 : TRASCINA);
preso.campioni.push({ v: passoDito, t: performance.now() });
if (preso.campioni.length > 5) preso.campioni.shift();
});
const lascia = (e) => {
if (!preso || e.pointerId !== preso.id) return;
if (preso.bloccato === 'x' && preso.campioni.length > 1 && !RIDOTTO) {
const c = preso.campioni;
const tempo = Math.max(1, c[c.length - 1].t - c[0].t);
const v = c.reduce((s, x) => s + x.v, 0) / tempo;
if (Math.abs(v) > 0.15) momento = v * LANCIO;
}
galleria.classList.remove('presa');
preso = null;
};
addEventListener('pointerup', lascia);
addEventListener('pointercancel', lascia);
/* il trackpad di lato (o maiuscolo + rotella) sposta la fila */
galleria.addEventListener('wheel', (e) => {
const lato = Math.abs(e.deltaX) > Math.abs(e.deltaY) * 1.75 ? e.deltaX : e.shiftKey ? e.deltaY : 0;
if (!lato) return;
e.preventDefault();
bersaglio -= lato * 0.8;
}, { passive: false });
galleria.addEventListener('pointerenter', (e) => { if (e.pointerType === 'mouse') sopra = true; });
galleria.addEventListener('pointerleave', () => { sopra = false; });
/* le frecce: un quadro alla volta, fermandosi sul più vicino */
const vai = (verso) => {
momento = 0;
bersaglio = Math.round((bersaglio - verso * passo) / passo) * passo;
};
document.querySelector('[data-avanti]').addEventListener('click', () => vai(1));
document.querySelector('[data-indietro]').addEventListener('click', () => vai(-1));The row's container rotates by -8 degrees and each picture by +8: the track runs along the diagonal, the images stay standing. The offset is applied to the track inside the rotated container, so it moves along the diagonal and not horizontally.
The originals sit between two sets of copies. The position goes through gsap.utils.wrap(-loop/2, loop/2), where the loop is the width of one set: when it leaves on one side it comes back on the other, and with identical copies the jump cannot be seen.
Page scrolling, dragging, throwing, arrows and drift all change the same target; every frame the real position follows it with a frame-rate independent brake (0.08 while dragging, 0.12 after).
While dragging, the last five moves are kept; on release their average speed times 1200 becomes a momentum added to the target, losing 6% every sixtieth of a second.
| Name | File | Default | What it changes |
|---|---|---|---|
ACCOPPIATA | script.js | 1 | Pixels sideways for each pixel of page scrolled. With 0 the row only moves when dragged. |
TRASCINA | script.js | 2.5 | How far the row runs compared to the finger. |
LANCIO | script.js | 1200 | Strength of the throw when you let go. |
DERIVA | script.js | 18 | Pixels per second of movement on its own when nobody touches it. With 0 it stays still. |
--inclinazione | style.css | -8deg | How steeply the row rises; -4 degrees on phones. |
To show a collection of large images (work, products, rooms) in a section you cross while scrolling, without pinning the page.
The pictures become almost as wide as the screen and the tilt drops to four degrees. A vertical finger scrolls the page, a horizontal one drags the row.
The row stays tilted but still: no drift, no link to scrolling and no inertia. It drags one to one and the arrows jump to the next picture.
GSAP 3.15.0, Mona Sans (font variabile) 5.3.0.
Weight: 16 KB of our own files.
Tested on 27 September 2026
Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.
With your account’s email and password.
You’re signed in as .
Your plan is free. When lifetime access arrives, it will switch on for this same account.