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");
}
});
});
});
})();-
Tristique et netus
Tristique et netus
Habitant morbi clem fandango 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.
Note: In this example, only the button is clickable because the tile contains specific interactive elements, like hyperlinks, which should never be obscured. By default, the entire tile region is clickable.
<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 <a href="#!">clem fandango</a> 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">
Pellentesque habitant morbi tristique senectus et netus et <a href="#!">malesuada fames</a> 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");
}
});
});
});
})();-
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.
Habitant morbi tristique senectus et netus. 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.
Note: In this example, each tile is allowed to be opened. By default, opening a new tile will close any others that are open.
<ul class="tile" data-open-all 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="card__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>
<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">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!