Expanding Grid Cards with View Transitions Chris Coyier

A friend showed me something of a design spec they were charged with pulling off: The requirements: when I click on any of them it expands at the top to full width and the rest of the cards re-arrange themselves below it.

A friend showed me something of a design spec they were charged with pulling off:

The requirements:

… when I click on any of them it expands at the top to full width and the rest of the cards re-arrange themselves below it.

Like anything web design & development, there are a boatload of ways you could do this. Different layout strategies, different combinations of native technologies, different libraries to punt to, etc. I lean native first, so this was how my brain scripted the problem:

  • Six cards on a fluid, flex-column CSS Grid. Let cards auto flow onto the grid.
  • When you click a card (instant 😬), donk it at the top by order: -1; (another 😬)
  • Make it full width regardless of columns via grid-column: 1 / -1;
  • I suppose we could animate this “for free” FLIP-style with the View Transition API.
  • And so:

    Video here. Note that only Chrome supports View Transitions. This video shows them, but the rest of it should work regardless of browser.

    The “alternate look” shows off how a little :has() selector can go a long way in interactive fun!

    The Grid

    Pretty easy these days, so I’ll make it complicated just for fun:

    .grid { display: grid; grid-template-columns: repeat( auto-fit, /* fluid columns, but max out at 3 */ minmax(min(100%, max(10rem, 100%/4)), 1fr) ); gap: 2.5rem; }Code language: SCSS (scss)

    This incantation of grid-template-columns says:

  • This grid is is fluid (the columns are of a fluid width)
  • The number of columns is fluid, starting at just 1, which shrinks as narrow as it needs to (it’s not limited by some fixed minimum width)
  • Only go up to 3 columns, not unlimited as you normally see
  • Break into two columns at a reasonable middle ground
  • A bit complex, but a useful bit of code there that can be expressed without media queries. I’m sure I’ll come back to this blog post for that over and over.

    The Move

    To make sure the clicked card becomes the first one, I’ll toggle a class:

    .card { ... &.featured { order: -1; grid-column: 1 / -1; font-size: 133%; } }Code language: SCSS (scss)
    const cards = document.querySelectorAll(".card"); cards.forEach((card, i) => { card.addEventListener("click", () => { activateCard(card); }); }); function activateCard(card) { cards.forEach((card) => { card.classList.remove("featured"); }); card.classList.add("featured"); }Code language: JavaScript (javascript)

    Animate It

    This is essentially a tweening (sometimes called FLIP) animation where you don’t really care about the starting or ending state per se you just say: animate between them please.

    The trick there is the View Transitions API, which means you just wrap any DOM manipulation in there and it’ll automatically animate. Here I’ll ensure the DOM changes even if the browser doesn’t have that API yet.

    if (!document.startViewTransition) { activateCard(card); } document.startViewTransition(() => { activateCard(card); });Code language: JavaScript (javascript)

    That’s ridiculously simple. I love it.

    I popped a close button on each card also which just de-features all cards, and calls the same API.

    The Accessibility Situation

    I’ve attached a click handler to a damn <div> here. That’s not right. A user using a screen reader won’t be able to activate the animation because the <div> is not interactive itself.

    But… maybe that’s OK?

    This is what you might call a “fidget spinner animation”. It doesn’t do anything important. No information is revealed. It’s just something you can click on for poops and giggles.

    The second we reveal some additional information… then we’re in trouble here. Then we need to attach the activation to an interactive element, probably a <button>. And as soon as we do that then the fact that we’re changing the tabbing order by messing with order becomes problematic. Tabbing order is no longer intuitive when the 4th item can appear at the top yet send focus to the 5th item just because that’s where it is in the DOM. Perhaps you could fight that by setting tabindex values ourselves, but that seems like a can of worms. Honestly, if this was revealing extra information, I’d probably just not take this approach at all and find some other more suitable way.

    Text selection is a minor concern. It’s a little annoying how you can select text and un-clicking after dragging activates the click and the text goes flying away from you. I say minor because the text remains selected so it’s not a 100% blocker.

    Another form of accessibility is not making people nauseous, so might as well honor the preference for reduced motion. Easy as CSS:

    @media (prefers-reduced-motion: reduce) { ::view-transition-old(*), ::view-transition-new(*) { animation-duration: 0s; } }Code language: CSS (css)

    Another little View Transitions Thing

    A hamburger interaction where the lines of the hamburger fly out to become the borders of the navigation drawer. This one not only uses View Transitions but native CSS nesting too just for fun.

    ncG1vNJzZmibmKe2tK%2FOsqCeql6jsrV7kWlpbGdgaXxyfY6er6mZnpm2r7OMoKminF2YrrOw0mauoqyYYsOqsdZmq6uZnqi2tbXOp6po

     Share!