Skip to main content

Tile v1.2.1

Group content in a visually organized layout, with the ability to flip and reveal additional information when a button is pressed.

  • Tristique et netus

    Tristique et netus

    Habitant morbi tristique senectus et netus. Unde omnis iste natus error sit voluptatem accusantium doloremque.

  • Tristique senectus et netus

    Tristique senectus et netus

    Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque.

  • Fames ac turpis egestas

    Fames ac turpis egestas

    Pellentesque habitant tristique senectus et malesuada fames ac egestas. Sed ut perspiciatis unde omnis iste error sit voluptatem doloremque.

<ul class="tile" role="list">

    <li class="tile__item">

        <div class="tile__panel">

            <div class="tile__cta">

                <div class="tile__label">Tristique et netus</div>

            </div>

            <div class="tile__content">

                <h2 class="tile__title">Tristique et netus</h2>

                <div class="tile__summary">
                    
                    <p>Habitant morbi tristique senectus et netus. Unde omnis iste natus error sit voluptatem accusantium doloremque.</p>

                </div>

            </div>

        </div>

    </li>

    <li class="tile__item">

        <div class="tile__panel">

            <div class="tile__cta">

                <div class="tile__label">Tristique senectus et netus</div>

            </div>

            <div class="tile__content">

                <h2 class="tile__title">Tristique senectus et netus</h2>

                <div class="tile__summary">
                    
                    <p>Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque.</p>

                </div>

            </div>

        </div>

    </li>

    <li class="tile__item">

        <div class="tile__panel">

            <div class="tile__cta">

                <div class="tile__label">Fames ac turpis egestas</div>

            </div>

            <div class="tile__content">

                <h2 class="tile__title">Fames ac turpis egestas</h2>

                <div class="tile__summary">
                    
                    <p>Pellentesque habitant tristique senectus et malesuada fames ac egestas. Sed ut perspiciatis unde omnis iste error sit voluptatem doloremque.</p>

                </div>

            </div>

        </div>

    </li>

</ul>
/*!

  Radancy Component Library: Card Flip

  Contributor(s):
  Michael "Spell" Spellacy

  Dependencies: Sass

*/

// Card (.tile)

.tile {

// Component Variables

--tile-base-color: #6F00EF;
--tile-transition: 0.8s;
--tile-columns: 3;

display: grid;
gap: 1.5em;
list-style: none;
margin: 0;
padding: 0;
text-wrap: pretty;
  
  @media (min-width: 60em) {

    grid-template-columns: repeat(var(--tile-columns), 1fr);

  }

  // Item (.tile__item)
  
  &__item {
  display: grid;
  perspective: calc(1500rem / 16);
  position: relative; // Needed for pseudo-element trick.
  }

  // Panel (.tile__panel)

  &__panel {
  display: grid;
  grid-area: 1 / 1;
  min-height: calc(320rem / 16);
  transform-style: preserve-3d;
  transition-property: transform;
  will-change: transform;

    @media (prefers-reduced-motion: no-preference) {

      transition-duration: var(--tile-transition);

    }

    .show & {
    transform: rotateY(180deg);
    }

  }

  // Button (.tile__trigger)

  &__trigger {
  background-color: var(--tile-base-color);
  border-radius: 50%;
  border: 0;
  color: #fff;
  cursor: pointer;
  font-size: 1.2rem;
  grid-area: 1 / 1;
  height: calc(40rem / 16);
  justify-self: end;
  margin-block-start: 1em;
  margin-inline-end: 1em;
  width: calc(40rem / 16);
  z-index: 1;
    
    // Button Icon

    &[aria-pressed="false"] {
    
      &::after {
      content: "+";
      }

    }

    &[aria-pressed="true"] {
    
      &::after {
      content: "-";
      }

    }

  }

  // Label (.tile__label)

  &__label {
  font-size: 2rem;
  font-weight: bold;
  line-height: normal;
  place-self: center;
  }

  // Card (.tile__cta & .tile__content)

  &__cta, &__content {
  background-color: #fff;
  backface-visibility: hidden;
  border: 1px #767676 solid;
  border-radius: 5px;
  display: grid;
  grid-area: 1 / 1;
  padding: 1em;
  transition: var(--tile-transition);
  }

  // CTA (.tile__cta)

  &__cta {
  background-color: #eee;
  opacity: 1;
  visibility: visible;
  text-align: center;

    .show & {
    opacity: 0;
    visibility: hidden;
    }

  }

  // Content (.tile__content)

  &__content {
  align-content: start;
  opacity: 0;
  transform: rotateY(180deg);
  visibility: hidden;

    .show & {
    opacity: 1;
    visibility: visible;
    }

  }

   // Summary (.tile__summary)

  &__summary {

    p  {
    margin: 0;
      
      &:not(:last-child) {
      margin-block-end: 1rem;
      }
  
      &:last-child {
      margin-block-end: 0;
      }
  
    }

  }

  // Clickable Region
  
  // Note: Only allow entire region to be clickable when certain elements are not present in card. 
  // This should cover most use cases, but do add new conditions as needs arise.

  &:not(:has(a)):not(:has(video)):not(:has(.tile__panel button)) {

    .tile__trigger {

      // Pseudo-element Trick
  
      &::before {
      content: "";
      display: block;
      inset: 0;
      position: absolute;
      z-index: 1;
      }

    }

  }
  
}
/*!

  Radancy Component Library: Tile

  Contributor(s):
  Michael "Spell" Spellacy

*/

(function() {

  "use strict";

  // Display which version is in use via console:

  console.log("%cTile%cv1.2.1", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");

  // Classes, data attributes, states, and strings.

  var tileClass = ".tile";
  var tileItemClass = ".tile__item";
  var tileLabelClass = ".tile__label";
  var tileTriggerName = "tile__trigger";
  var tileTriggerClass = "." + tileTriggerName;
  var tileTriggerLabel = "Show Content";
  var tileState = "show";
  var dataTileOpen = "data-open-all";
  var tiles = document.querySelectorAll(tileClass);

  // For each component

  tiles.forEach(function(tile, e){

    var tileCount = e + 1;

    var cards = tile.querySelectorAll(tileItemClass);

    cards.forEach(function(card, i){
      
      // ID for each label
      
      var cardCount = i + 1;
      var tileLabel = card.querySelector(tileLabelClass);

      if(tileLabel) {
      
        tileLabel.setAttribute("id", "tile-item-" + tileCount + "-" + cardCount);

      }

      // Create button
      // Note: Button must always be appended to card, never before.

      var tileButton = document.createElement("button");
      tileButton.setAttribute("aria-pressed", "false");
      tileButton.classList.add(tileTriggerName);
      tileButton.setAttribute("aria-label", tileTriggerLabel);

      if(tileLabel) {

        tileButton.setAttribute("aria-describedby", "tile-item-" + tileCount + "-" + cardCount);

      }
      
      card.prepend(tileButton);

      tileButton.addEventListener("click", function() {

        // Get all tile items and buttons

        var tileItems = this.closest(tileClass).querySelectorAll(tileItemClass);
        var tileButtons = this.closest(tileClass).querySelectorAll(tileTriggerClass);
        var tileItemParent = this.parentNode;

        if(tileItemParent.classList.contains(tileState)) {

          tileItemParent.classList.remove(tileState);
        
          this.setAttribute("aria-pressed", "false");

        } else { 

          // Remove class from all tile items

          if(!this.closest(tileClass).hasAttribute(dataTileOpen)) {

            tileItems.forEach(function(item) {

              item.classList.remove(tileState);

            });

            tileButtons.forEach(function(btn) {

              btn.setAttribute("aria-pressed", "false");

            });

          } 

          tileItemParent.classList.add(tileState);
          this.setAttribute("aria-pressed", "true");

        }

      });

    });

  });

})();

Properties Anchor (Properties)

Use the following properties to configure this component.

Attribute Applies To Note Description
data-open-all .tile Boolean, Optional
Used to allow all cards to be opened. By default, opening a new card will close any others that are open.

Class Applies To Note Description
.tile ul, div Mandatory
Used on the parent element that wraps your card. Standalone cards should use a div element.
.tile__item li, div Mandatory
Used on your card item. Must be a child of .tile. If your card is a standalone, then use a div.
.tile__panel div Mandatory
Used to wrap card content. Must be a child of .tile__item.
.tile__cta div Mandatory
The front of the card. Always a child of .tile__panel.
.tile__label div Optional
The card label that appears on front of the card. Always a child of .tile__cta. Optional but strongly recommended, as its presence ensures each generated button includes an accessible description via aria-describedby.
.tile__content div Mandatory
The back of the card. Always a child of .tile__panel.
.tile__title h2, h3 Optional
The heading on the back of the card.
.tile__summary div Optional
The content container on the back of the card.
.tile__trigger button Component
The cards interaction trigger. Always a child of .tile__item. Includes a default name of "Show Content." When tile__label is present, button will receive an additional description. Each tile may only contain one trigger.

Release Notes Anchor (Release Notes)

Keep up to date on major changes to this component.

Bug
July 1, 2026: Prepended the tile button so it does not affect the focus order. Originally, the button was prepended, but a client requested that it be placed after the content. This change disrupted the tab order, so it has been reverted to its original position before the content. Version increment to 1.2.1.

Bug
Paul raised a somewhat serious animation quirk displayed in this component when the viewport is resized. A temporary solution is available, but a permanenet solution will be required. More information can be found on CodePen. (High)

Report Issues

Find a bug? Want a new feature? Report it on JIRA. You're so awesome!