← The library

Fan carousel

Cards spread out in a fan

Members only

What's included

A hand of cards spread out in a fan, in two files: a stylesheet and a script. No dependencies, no build step, no external requests — you copy them into a project and it works.

Each card moves aside, drops, tilts and shrinks in proportion to its distance from the chosen card. That arithmetic is done in CSS, from a single number: the index of the checked radio button, which :has() publishes into a variable. That's why the fan is in place before the script arrives, and stays there if the script never comes.

Features

  • The state lives in the document — a checked input, not a JavaScript variable. The CSS reads it, the keyboard moves through it, a form would submit it as it stands.
  • Full keyboard support, without a line of code — they're native radio buttons: the arrow keys move from card to card, Tab steps into and out of the group, and the focus ring lands on the dot.
  • Works without JavaScript — the fan is spread out, the cards are clickable, the dots respond. The script only adds dragging by finger and by mouse.
  • One variable for the scale — card, arc, tilt, corner radius, shadows and the five text sizes all derive from --arc-font-size.
  • The original spring, in CSS — the linear() curve is sampled from the reference's spring, its 8.3% overshoot included. Measured path: 0.67 px of average difference over a 214 px run.
  • It adapts to its container, not to the window — the thresholds are @containerqueries, so a fan dropped into a narrow column tightens up even on a large screen.

Dropping it into a page

Two files to load, then the markup:

<link rel="stylesheet" href="fan-carousel.css">
<script src="fan-carousel.js" defer></script>
<section class="fan-carousel" data-fan-carousel aria-label="Our plans">
  <header class="fan-carousel__intro">
    <p class="fan-carousel__eyebrow">Eyebrow</p>
    <h2 class="fan-carousel__headline">The headline</h2>
  </header>
  <div class="fan-carousel__stage">
    <article class="fan-carousel__card" role="group" aria-labelledby="t1">
      <div class="fan-carousel__frame">
        <img class="fan-carousel__image" src="…" alt="…">
        <div class="fan-carousel__caption">
          <div class="fan-carousel__text">
            <h3 class="fan-carousel__title" id="t1">Title</h3>
            <p class="fan-carousel__subtitle">Subtitle</p>
          </div>
          <span class="fan-carousel__tag">Label</span>
        </div>
      </div>
      <label class="fan-carousel__hit" for="c1" aria-hidden="true"></label>
    </article>
    <!-- … -->
  </div>
  <div class="fan-carousel__dots" role="radiogroup" aria-label="Card to bring forward">
    <input class="fan-carousel__radio" type="radio" name="deck" id="c1" checked>
    <label class="fan-carousel__dot" for="c1"><span class="fan-carousel__sr">Title</span></label>
    <!-- … -->
  </div>
  <div class="fan-carousel__probe" aria-hidden="true"></div>
</section>

One input per card, in the order of the cards: that's all the component asks for. The one checked at the start is the card that will be in front.

What you should know

  • The fan is wider than its frame. That's the design: the cards stick out on both sides. The stage clips them horizontally and lets them fall away downwards, so the component never gives the host page a scrollbar — which the reference does, by 31 px at 810 and by 131 px at 390.
  • Twelve cards at most. The CSS writes one rule per rank; beyond that the fan isn't readable anyway. Each instance needs its own name andid.
  • The arrow keys stay inside the component. They act when a dot has focus, because they're the native arrow keys of a radio group. The reference listened for them on window: they changed card even while you were typing into a field elsewhere on the page.
  • No inline styles, no inline scripts. The component honours the site's strict security policy, checked by serving it under the header, and never uses innerHTML.
  • The design comes from elsewhere. The proportions follow those of a commercial Framer component, measured on the published site at five widths. The code itself is original: nothing was taken from its implementation. The six photographs are those of the original demo, of unknown licence — replace them with your own.

Take it, open it, break it.

This component is free. The full library — the templates and the other components — comes with the subscription, alongside the course.

Immediate access · No commitment · Cancel in one click