36 Tailwind stacked-card patterns spanning scroll-driven pinned stacks, swipe and shuffle decks, fan-out photo piles, accordion and receipt reveals, 3D tilt and flip layers, isometric towers, and notification and testimonial stacks. Scoped under .stk-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Building a whole Tailwind app, not just this component?
You just browsed 36 Tailwind stacked cards. Untitled UI Pro unlocks 5,000+ components and sections and 250+ page examples for React, styled with Tailwind CSS v4. One licence is a one-time $349 with lifetime updates; teams start at $999.
The storytelling hero, written in the markup. Each card is sticky with a stepped offset computed from its own --i, so top-[calc(90px+var(--i)*16px)] replaces four :nth-child rules. The scale-and-darken is an @theme animation — animate-stk01-shrink — driven by the two arbitrary properties Tailwind has no utility for, [animation-timeline:view()] and [animation-range:exit]. The only rules left in a stylesheet are the keyframes themselves.
The decision deck, ported to variants. The depth of the pile stops being three :nth-last-child rules and becomes nth-last-2:, nth-last-3: and nth-last-[n+4]: typed straight onto the card. The fly-off is a data-gone attribute the script sets, with the two exit transforms written as important arbitrary utilities so they beat the inline transform the drag leaves behind. The LIKE / NOPE stamps become real spans driven by group-data-[hint=yes]:, not content strings in a stylesheet.
A frosted pile over a slowly drifting gradient. backdrop-blur-[16px] backdrop-saturate-150 is one of the few places Tailwind's utility is genuinely nicer than the property — the -webkit- prefix ships automatically. The resting scatter rides each card as --x/--y/--r, and the spread is a group-hover: / group-focus-within: pair of arbitrary transforms, so the whole choreography sits on the three cards instead of in six selectors.
A pile resting on its bottom edges that opens by animating grid rows, no JS. The 0fr → 1fr reveal is grid-rows-[0fr] with peer-checked:grid-rows-[1fr] — a real utility pair, not an arbitrary property. The open card lifts clear via has-checked:, which is exactly the :has() rule the CSS version needs, only typed on the panel it applies to.
A tidy pile that fans into an arc, where the arithmetic replaces the rule. Every photo carries --n — its signed position from the centre — and one shared class list turns that into both the rotation and the arc lift: group-hover:[transform:rotate(calc(var(--n)*15deg))_translateY(calc(var(--n)*var(--n)*-2px))]. Five identical class lists, five different destinations, no :nth-child map anywhere.
The holographic deck: five parallax planes, an iridescent conic sweep, a cursor-tracked specular glare and a mask-composite border, tilting toward the pointer in real time. This is the demo where the port costs the most — @property, @keyframes and the mask trick all stay in CSS — and the payoff is that every translateZ plane is now readable on the element it belongs to instead of five rules away.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
A wizard as a deck. The four [data-state] rules become four data-[state=…] variant sets typed on every card, so a step's whole life — waiting behind, in focus, sunk to the back — reads in one class list. Native constraint validation gates the advance, inert keeps focus out of the cards behind, and user-invalid: paints the error state without a single line of script.
A filing-cabinet deck where clicking a tab brings its card to the absolute front. The CSS version needs eight :nth-of-type pairings to connect radio n to tab n; the port throws all of them away by nesting each radio, tab and card together and using plain peer-checked:. Restructuring the markup is the port — and the result is shorter, flatter and far easier to extend.
A queue you browse by clicking the front card: it flings sideways, drops to the bottom of the pile and glides back in at the rear. Depth is a number, not DOM order — every card reads its own --depth through [transform:translateY(calc(var(--depth)*16px))_scale(calc(1-var(--depth)*0.05))] and [z-index:calc(10-var(--depth))], so the script only ever repaints one integer per card.
Depth from tone and shadow alone — no colour tricks, no 3D. Four cards share a footprint, stepped by [transform:translate(calc(var(--i)*5px),calc(var(--i)*6px))], and the thickness comes from a two-layer shadow: a hard 2px offset for the cut edge, a soft blur for the drop. On hover the same expression runs at a larger step, so one utility describes both the resting deck and the spread.
A poker hand, fanned from one anchor. Each card carries its own --a and every state repeats that angle: rest is [transform:rotate(var(--a))], raised is hover:[transform:rotate(var(--a))_translateY(-40px)_scale(1.05)]. Because the pivot is shared and low, spacing stays even however many cards you deal — adding a sixth is one angle, not a new rule.
Snapshots tossed on a table: thick white frames, handwritten captions, deliberate mess. Each snap's position is three numbers on the element (--x, --y, --r) and hovering zeroes the rotation while raising z-10, so the one you are looking at pops clean out of the pile. The captions carry a --font-* theme entry, which is how a second typeface becomes font-stk12-hand rather than a one-off rule.
A strict stair-step cascade where one uniform corner of every card stays visible. The whole staircase is a single expression — [transform:translate(calc(var(--i)*24px),calc(var(--i)*46px))] with [z-index:calc(var(--i)+1)] — and hovering adds a lateral offset to the same expression, so the step slides out along its own diagonal rather than jumping off it.
A packed avatar row that springs open the moment you point at it. The overlap is -ml-7 released to group-hover:ml-3.5, the overshoot is a named --ease-* token, and the ripple is a per-card delay-[calc(var(--i)*30ms)] — so the whole open-and-close lives in one class list rather than four rules and a delay map.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
The parting-of-the-waters stack: hover a card and every card below it slides down. This is the demo where Tailwind runs out of variants — there is no hover ~ sibling family — so the whole effect rides one arbitrary variant, [&:hover~*]:translate-y-[30px], typed on each card. Worth knowing precisely because it is the exception rather than the rule.
One checkbox unfurls a packed pile into a spaced menu. The reveal is the grid-rows-[0fr] → 1fr pattern again, but the interesting part is reaching the items: peer-* stops at siblings, so the links are addressed with peer-checked:[&_a]:opacity-100 — the child-selector bolt-on that is the single most common Tailwind port bug.
A deck riffling itself, offset per card by a negative animation-delay so the loop never syncs. Hover and it stops dead and fans out to be read — group-hover:[animation-play-state:paused] plus a static fan transform. The reduced-motion path is the interesting one: it does not slow the loop down, it removes it and ships the fanned deck as the resting state.
Manila dividers, done in utilities. The tab silhouette is [clip-path:polygon(8%_0,92%_0,100%_100%,0_100%)] — a shape with no utility and no need for one — and the same nesting trick as demo 08 retires six :nth-of-type pairings in favour of plain peer-checked:. Resting folders sit dimmed behind; the checked one comes forward at full strength.
Hover one photo and the whole cluster gathers toward the centre and dims, while the one you touched brightens and rises. The group behaviour is group-has-hover: — Tailwind's variant form of :has(:hover) — and the spotlight override needs the ! modifier, because the hovered photo has to beat a rule its own parent just applied to it.
A thumb-first media deck that loops forever. Depth is one --d per card feeding a translate3d and a scale; the drag writes a live translate3d inline; and the commit flings the card off and re-appends it to the back. The port's contribution is that [touch-action:none], will-change-transform and data-[drag=true]:transition-none now sit on the card, where anyone debugging a janky swipe will look first.
Three panels inside one card whose height never changes. The open section takes the space by growing its flex, not by getting taller — has-checked:flex-1 — so nothing outside the card reflows and CLS stays at zero. The status dot is a before: utility rather than a pseudo-element rule, which is the small win: the dot's colour change is visible on the header it belongs to.
Five floors on a steep isometric plane, each extruded by a before: slab so the column has real thickness. The shading is the interesting port: instead of five colour tokens, each floor computes its own fill from its index inside oklch() — calc(0.78-var(--i)*0.05) in the lightness slot — so the whole tonal ramp is one expression.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
A pricing card that turns 180° to show what is behind it. This is the port where v4's own 3D utilities finally cover the ground: transform-3d, backface-hidden, rotate-y-180 and perspective-distant are all real classes, so the flip needs no arbitrary properties at all. The reduced-motion path is the substantive part — it replaces the turn with a cross-fade rather than a slower turn.
A deck that snaps to one of four poses depending on which quadrant your pointer is in — crisper and far cheaper than continuous tilt. The script writes two unitless numbers and CSS multiplies them into degrees, which is the pattern worth stealing: rotateX(calc(var(--ty,0)*1deg)) keeps the JS free of units and the CSS free of parsing.
An OS-style alert pile: new notifications spring onto the top, older ones compress below, and one toggle switches the whole thing between a compact deck and a spaced list. The two layouts are one data-open attribute on the pile — data-[open=true]: variants flip every card from absolute-and-scaled to relative-and-flat, so there is no second stylesheet for the expanded state.
An order summary that unrolls like till paper. The reveal is the familiar grid-rows-[0fr] → 1fr pair, but the memorable part is the perforated edge: one arbitrary [mask:radial-gradient(9px_at_50%_0,transparent_98%,#000)_repeat-x_0_0/18px_12px] cuts the zig-zag out of a plain block, and the direction arrow is a peer-checked:after:content swap rather than two rules.
A foil coating you scratch away with a finger. The interesting boundary here is that Tailwind styles the frame and the canvas owns the pixels: the coating, the brush and the erase are Canvas 2D with destination-out, while the layout, the prize card, the crosshair cursor and the data-[done=true]:opacity-0 fade are all utilities. Knowing which side of that line a problem is on is most of the work.
The editorial portfolio deck: four gradient cards in a slight pile that fan into an arc when you approach, then lift individually. The port's real subject is the flicker guard — the fan must apply on stage hover except when a card itself is hovered, and no built-in variant expresses that, so it becomes one honest arbitrary variant. All four cards then share a single identical class list, with their destinations as custom properties.
The How-It-Works sequence: each step pins, and the one behind it scales down and dims as the next slides over. An IntersectionObserver sets one data-state per card — ahead, live, behind — and every visual difference is a data-[state=…] variant, which is both cheaper than scroll maths and far easier to read.
Three reviews, a cyclic stack, no JavaScript — and the port's headline feature is named peers. The mapping here is a rotation (radio 1 puts quote 2 second and quote 3 third; radio 2 rotates it on), which nesting cannot solve because each quote must respond to all three radios. peer/t1, peer/t2, peer/t3 plus peer-checked/t2: expresses it exactly, and nine positional selectors disappear.
Three pricing planes floated on a true isometric grid, sliding apart on hover so every price stays readable. The port keeps the geometry in arbitrary properties where it belongs, and the accessibility work is the substantive part: prices are legible at rest, and under reduced motion the whole plane flattens to rotate-none rather than merely animating slower.
A dashboard bento where one tile hides a stacked mini-deck: three channel breakdowns collapsed into a single cell that fans open on hover. This is the demo where Tailwind's grid utilities do the heavy lifting for free — col-span-2, row-span-2 and a fixed track list replace named grid areas entirely — while the fan itself is the index pattern one more time.
Four panels receding into the screen on negative Z, tilting as a group on hover while the front card pushes toward you. Four :nth-child rules become one index expression — depth, vertical offset and opacity all fall out of --i — and the opacity ramp doubles as the contrast fix, because a panel that recedes should also read as background.
A sticker-card that curls off its stack from the top-right corner, revealing the vivid layers beneath. The port swaps animated clip-path for a rotate-scale-and-radius peel, which is the better engineering call: clip-path only interpolates between polygons with matching vertex counts, while a transform plus one asymmetric border-radius gets the same read and stays on the compositor.
A gradient-spine timeline whose cards slide right and whose nodes bloom on hover. The port's contribution is semantic rather than visual: the div soup becomes a real <ol> with <time datetime> elements, and both the spine and the milestone nodes become before: utilities on the elements they belong to instead of two floating pseudo-element rules.
Five cards on a rotating helix, pausing on hover or focus. Every card's place on the spiral is one expression over its index — rotateY(calc(var(--i)*72deg))_translateZ(150px)_translateY(calc((var(--i)-2)*60px)) — so changing the card count is changing one number. The honest part of this entry is what it says about itself: a spinning tower cannot carry primary content, and the demo is marked accordingly.
FAQ
Frequently asked questions
How do you stack cards on top of each other in Tailwind CSS?
The base recipe is one positioning context plus a per-card offset. The container is relative; every card is absolute inset-0 so they share a footprint, then you separate them with translate-y and scale steps and order them with z-10, z-20 and so on. Demo 10 (Classic Card Deck) is the plain version: four cards stepped by translate-y-2 and layered with a stacked shadow so depth reads from tone alone. When the offset should scale with a card's position rather than a fixed step, hand each card a CSS variable such as --n or --d and multiply inside an arbitrary value like [transform:translateY(calc(var(--n)*0.5rem))]. That one expression replaces a run of nth-child rules and is how the fan and tower demos avoid hardcoding every single layer.
How do I make cards stack as I scroll using position sticky?
Each card is sticky top-0 — or a stepped top offset — inside a tall scroll container, so as the page moves each card pins in turn and the next slides over it. Demo 29 (Scroll Activated Sticky Stacking) and Demo 01 (Scroll-Driven Scaling) build a How-It-Works sequence this way: the pinned card scales down and dims with scale and reduced opacity while the one behind it takes the foreground. The load-bearing detail is that no ancestor between the card and the scroll root may set overflow-hidden — a clipping parent kills sticky silently, and it is the single most common reason a sticky stack refuses to pin. Give the container real height, roughly one screen per card, so there is scroll distance for the effect to play out, and pair the transforms with a motion-reduce: variant so the sequence degrades to a plain scroll for reduced-motion users.
Why are my stacked cards not clickable or the wrong one showing on top?
Two causes dominate. First, z-index only applies to positioned elements, so a card with no relative or absolute and no z- utility stacks purely in DOM order regardless of intent — add the position utility and an explicit z-10 or z-20. Second, an absolute inset-0 card covers its siblings and swallows every pointer event, so a button on a lower card never receives the click even though you can see it. Demo 08 (Tabbed, Bring to Front) and Demo 09 (Click to Front) fix this by raising the active card's z- on peer-checked: so the interactive one is genuinely on top, and by letting inert layers pass clicks through with pointer-events-none while the front card keeps pointer-events-auto. If a hover transform makes a card jump, check that a transform has not created a new stacking context that reorders siblings.
How do I fan out stacked cards on hover without JavaScript?
Put the trigger on the container and let the children react. Wrap the pile in a group, then each card responds with group-hover: variants that release its offset — the tidy pile becomes an arc. Demo 05 (Fan Out Photo Stack) and Demo 28 (Hover Reveal Spread) give every card its own angle in a variable like --a and rotate by group-hover:[transform:rotate(var(--a))], so one rule fans any number of cards instead of a hand-written value per photo. Demo 14 packs an avatar row with negative -ml-7 and releases the overlap to group-hover:ml-0. Add transition and a duration so the spread eases rather than snaps, and remember hover is pointer-only: back it with a focus-within: variant so keyboard and touch users can trigger the same spread.
Are stacked card layouts accessible and keyboard-navigable?
They can be, but stacking hides content by design, so the rules matter. Anything a user must reach — a review behind the front card, a form step, a dismiss control — has to be a real focusable element, and its card must not be dropped from the tab order or buried under an absolute sibling that intercepts focus. Where a card is genuinely off-screen or collapsed, mark it so assistive tech skips it rather than reading a pile of overlapping text. The pure-CSS toggles in Demo 07 (Multi Step Form), Demo 25 (Notification Stack) and Demo 30 (Testimonial Slider) are driven by real <input> radios and checkboxes with matching <label> elements, so they are keyboard-operable for free. Every continuous or hover animation should carry a motion-reduce: variant that removes the movement, and every hover-only reveal needs a focus-within: equivalent so the content is reachable without a mouse.
When should I not use a stacked card layout?
Skip it whenever the user needs to compare items side by side — pricing tiers, search results, a data table — because stacking deliberately hides all but the top card and forces a sequential reveal. It is also wrong for primary navigation or content that must be immediately visible, since off-screen or collapsed cards read as buried. On small screens a deep pile with large translate and rotate offsets can overflow the viewport or leave tap targets overlapping; the mobile-first demos (Demo 20 swipe deck, Demo 09 cycle) keep offsets small and show one card at a time for exactly that reason. And 3D demos that lean on perspective and heavy backdrop-blur (Demo 06, Demo 33, Demo 03) are GPU-costly — fine as a hero, wasteful for a list of twenty. Reach for a plain grid when the job is scanning, not storytelling.
How does the grid-rows 0fr to 1fr accordion trick work in Tailwind?
It animates height without knowing the content height in advance, which a plain height transition cannot do. The collapsed card is a grid row of grid-rows-[0fr] wrapping a child that carries overflow-hidden; on open you switch the track to grid-rows-[1fr] and the row grows to exactly the content's natural size, with the transition on grid-template-rows doing the easing. Demo 04 (Expand Accordion), Demo 16 (Slide Out Menu) and Demo 26 (Receipt Roll) all use this, toggled by a peer-checked: or group-has-[:checked]: variant so it stays JavaScript-free. The one gotcha: the immediate child must have overflow-hidden and no vertical margin or padding of its own, or the content spills while collapsed and the animation looks broken. Browsers without animatable grid tracks simply jump instead of easing, which is an acceptable degrade.
Do these stacked cards work without JavaScript?
Most do. Roughly two thirds are pure CSS driven by hidden form controls: a set of <input> radios or a checkbox, paired <label> triggers, and Tailwind's state variants reading them. Demo 30 (Testimonial Slider) cycles three reviews with named radio peers using v4's peer/name syntax so each label targets a specific input; Demo 08 and Demo 09 bring a card to front with peer-checked:z-30; Demo 25 toggles a notification pile with one checkbox. The demos that genuinely need script — a drag-to-dismiss swipe (Demo 02), a canvas scratch mask (Demo 27), pointer-quadrant tracking (Demo 24) — ship their handler in the JS tab, and every card is tagged so you know before you copy whether a snippet is vanilla or needs a listener. Scroll-driven stacks are pure CSS too, relying on sticky rather than a scroll event.