# Venetian Blind > Two CSS mask transitions that split a slide into vertical strips — one collapses the outgoing image, the other reveals the incoming one. No JS animation library. Live: https://vanilla-motion.com/venetian-blind/ · Tuning app: https://vanilla-motion.com/venetian-blind/app/ · Code reference: https://vanilla-motion.com/venetian-blind/skill.md · Params: https://vanilla-motion.com/venetian-blind/params.json Two variations of a venetian-blind transition for full-bleed image slideshows — both built entirely with CSS `mask` and `@keyframes`, no animation library. The effect splits a slide into 5 vertical strips that collapse or expand with a staggered timing, creating a mechanical reveal that works well for editorial and portfolio contexts. ## Disappear: outgoing slide collapses Each strip on the outgoing slide shrinks toward its vertical center, exposing the next image beneath. Five `@property`-registered custom properties (`--p1`…`--p5`) animate from `0` to `1` via `@keyframes`. Each drives a column of the mask, shrinking the visible region toward center: ```css /* p=0 → strip fully visible (t=0%, b=100%) */ /* p=1 → strip collapsed to center (t=50%, b=50%) */ --t: calc(var(--p) * 50%); --b: calc(100% - var(--p) * 50%); mask: linear-gradient(to bottom, transparent 0 var(--t), #000 var(--t) var(--b), transparent var(--b)) 0% 0 / calc(100% / 5 + 1px) 100% no-repeat; ``` The `+1px` on column width closes rasterization gaps at non-integer device pixel ratios. `.active` goes on the **outgoing** slide (`direction: "collapse"`); after `animationend` it's removed and the slide drops to `z-index: 0`. ## Reveal: incoming slide expands Same strips, reversed direction — the incoming slide expands outward from center rather than collapsing inward. > ▶ [Live demo](https://vanilla-motion.com/venetian-blind/embed/reveal/) · [open in the tuning app](https://vanilla-motion.com/venetian-blind/app/) Two things differ from the disappear variant: 1. **`.active` targets the incoming slide**, not the outgoing. The outgoing stays at `z-index: 1` beneath, fully visible until covered. 2. **Strip formula is inverted**: starts at center, expands to edges. ```css /* p=0 → strips at center (zero height) → invisible */ /* p=1 → strips fill full height → fully visible */ --t: calc(50% - var(--p) * 50%); --b: calc(50% + var(--p) * 50%); ``` With `direction: "reveal"` non-active slides have **no mask applied** — they're fully visible by default. Only the incoming `.active` slide carries the mask during its animation. In the packaged effect both variants are one class, switched by the `direction` param. A single `@property --vm-vb-p` runs 0 to 1 over `duration`, and the mask built for each slide derives every strip's own progress from it (`stagger` percent later per strip, eased in as t squared), so the strip count is a param too. The direction is purely determined by the `--t`/`--b` formula. ## Why @property Custom properties are strings by default — they can't be interpolated by `@keyframes`. Registering them as `` via `@property` makes them numeric and animatable: ```css @property --p1 { syntax: ""; inherits: false; initial-value: 0; } ``` Without this, the mask would snap from start to end on the first frame instead of animating. ## Why no JS animation library The script only manages state: track current index, raise the animated slide's `z-index`, toggle `.active`. All interpolation happens in CSS. Zero dependencies. ## Accessibility ```ts if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) { mount(el, { images }) } ``` When `prefers-reduced-motion` is set, skip `mount` and keep the first image as a static ``. Slides stay static.