/* Flip profile cards: two faces stacked in one grid cell. A button on each face turns the card (Y axis, or X with .fp-x);
the hidden face is made inert so it can't be tabbed into, focus moves to the face that comes up, and Escape turns it back.
Without JavaScript the faces simply stack. Under prefers-reduced-motion the turn becomes a crossfade. */
.fp{--fp-accent:#0369a1;--fp-ink:#0f172a;--fp-muted:#475569;--fp-line:#e2e8f0;--fp-back:#0f172a;--fp-back-ink:#e2e8f0;--fp-back-muted:#a5b4c8;--fp-cover:linear-gradient(120deg,#38bdf8,#6366f1);
height:100%;perspective:1400px;color:var(--fp-ink)}
.fp :where(h3,p,ul,dl,dd){margin:0;padding:0}
.fp-inner{display:grid;gap:16px;height:100%}
.fp-face{position:relative;display:flex;flex-direction:column;border-radius:22px;overflow:hidden}
.fp-face:focus{outline:none}
.fp-face:focus-visible{outline:3px solid var(--fp-accent);outline-offset:3px}
.fp-front{background:#fff;border:1px solid var(--fp-line);box-shadow:0 24px 40px -30px rgba(15,23,42,.45);text-align:center}
.fp-cover{height:86px;background:var(--fp-cover)}
.fp-av{display:block;width:96px;height:96px;margin:-48px auto 0;border-radius:50%;overflow:hidden;border:4px solid #fff;box-sizing:content-box}
.fp-av svg{display:block;width:100%;height:100%}
.fp-body{display:flex;flex-direction:column;align-items:center;flex:1;padding:12px 22px 22px}
.fp-name{font-size:1.15rem;font-weight:800;letter-spacing:-.01em}
.fp-role{margin-top:4px;font-size:.9rem;color:var(--fp-muted)}
.fp-tags{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;list-style:none;margin:16px 0 20px}
.fp-tags li{padding:4px 10px;border-radius:999px;background:#f1f5f9;color:#334155;font-size:.78rem;font-weight:600}
.fp-turn{display:inline-flex;align-items:center;justify-content:center;gap:8px;margin-top:auto;padding:10px 16px;border:1.5px solid var(--fp-accent);border-radius:12px;background:transparent;color:var(--fp-accent);
font:inherit;font-size:.88rem;font-weight:700;cursor:pointer;transition:background-color .2s,color .2s}
.fp-turn svg{width:16px;height:16px}
.fp-turn:hover{background:var(--fp-accent);color:#fff}
.fp-turn:focus-visible{outline:3px solid var(--fp-accent);outline-offset:3px}
.fp-back{padding:24px 22px 22px;background:var(--fp-back);color:var(--fp-back-ink);box-shadow:0 24px 40px -26px rgba(15,23,42,.6);text-align:left}
.fp-k{font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--fp-back-muted)}
.fp-back .fp-name{margin-top:6px;color:#fff}
.fp-bio{margin-top:10px;font-size:.9rem;line-height:1.55}
.fp-facts{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;margin-top:16px;font-size:.84rem}
.fp-facts dt{color:var(--fp-back-muted)}
.fp-facts dd{font-weight:600}
.fp-links{display:flex;gap:8px;list-style:none;margin:18px 0 20px}
.fp-links a{display:inline-flex;align-items:center;gap:6px;padding:7px 11px;border-radius:9px;background:rgba(255,255,255,.08);color:#fff;font-size:.82rem;font-weight:600;text-decoration:none}
.fp-links a:hover{background:rgba(255,255,255,.16)}
.fp-links a:focus-visible{outline:3px solid #7dd3fc;outline-offset:2px}
.fp-links svg{width:15px;height:15px}
.fp-back .fp-turn{align-self:flex-start;border-color:#7dd3fc;color:#7dd3fc}
.fp-back .fp-turn:hover{background:#7dd3fc;color:var(--fp-back)}
.fp-back .fp-turn:focus-visible{outline-color:#7dd3fc}
/* the turn, once the script has marked the card ready */
.fp-turn{display:none}
.fp.is-ready .fp-turn{display:inline-flex}
.fp.is-ready .fp-inner{gap:0;transform-style:preserve-3d;transition:transform .7s cubic-bezier(.2,.7,.2,1)}
.fp.is-ready .fp-face{grid-area:1/1;-webkit-backface-visibility:hidden;backface-visibility:hidden}
.fp.is-ready .fp-back{transform:rotateY(180deg)}
.fp.is-flipped .fp-inner{transform:rotateY(180deg)}
.fp-x.is-ready .fp-back{transform:rotateX(180deg)}
.fp-x.is-flipped .fp-inner{transform:rotateX(180deg)}
/* colour themes */
.fp.t-rose{--fp-accent:#be123c;--fp-cover:linear-gradient(120deg,#fb7185,#f59e0b)}
.fp.t-teal{--fp-accent:#0f766e;--fp-cover:linear-gradient(120deg,#2dd4bf,#3b82f6)}
/* landscape speaker card */
.fp-x .fp-front{display:grid;grid-template-columns:auto 1fr;align-items:center;align-content:center;gap:6px 22px;padding:24px;text-align:left}
.fp-x .fp-av{grid-row:span 4;margin:0;border:0;width:104px;height:104px;border-radius:26px}
.fp-x .fp-body{display:contents}
.fp-x .fp-name{font-size:1.25rem}
.fp-talk{grid-column:2;font-size:.95rem;font-weight:600;line-height:1.4}
.fp-x .fp-front .fp-turn{grid-column:2;justify-self:start;margin-top:8px}
.fp-x .fp-back{display:grid;grid-template-columns:1fr auto;align-items:end;gap:16px 24px;padding:24px}
.fp-x .fp-facts{margin-top:12px}
.fp-x .fp-links{margin:0;flex-direction:column}
.fp-x .fp-back .fp-turn{grid-column:1/-1;justify-self:start}
@media (max-width:520px){.fp-x .fp-front,.fp-x .fp-back{grid-template-columns:1fr}.fp-x .fp-av{grid-row:auto}.fp-x .fp-front>*,.fp-talk,.fp-x .fp-front .fp-turn{grid-column:1}}
.fp .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (prefers-reduced-motion:reduce){
.fp-turn{transition:none}
.fp.is-ready .fp-inner,.fp.is-flipped .fp-inner{transform:none;transition:none}
.fp.is-ready .fp-face{-webkit-backface-visibility:visible;backface-visibility:visible;transform:none;transition:opacity .25s,visibility 0s .25s}
.fp.is-ready:not(.is-flipped) .fp-front,.fp.is-flipped .fp-back{transition:opacity .25s,visibility 0s}
.fp.is-ready .fp-back,.fp.is-flipped .fp-front{opacity:0;visibility:hidden}
.fp.is-flipped .fp-back{opacity:1;visibility:visible}
}
JavaScript
/* Flip cards: each .fp-turn turns its card over. The face that is turned away gets the inert attribute (no focus, hidden from
screen readers). Turning to the back moves focus to the back face so it is read from the top; turning to the front (with its
button or Escape) returns focus to the button that opened it. */
document.querySelectorAll('.fp').forEach(function (card) {
var front = card.querySelector('.fp-front'), back = card.querySelector('.fp-back');
var opener = front.querySelector('.fp-turn');
function show(flipped, moveFocus) {
card.classList.toggle('is-flipped', flipped);
front.inert = flipped; back.inert = !flipped;
opener.setAttribute('aria-expanded', String(flipped));
if (moveFocus) (flipped ? back : opener).focus({ preventScroll: true });
}
card.querySelectorAll('.fp-turn').forEach(function (btn) {
btn.addEventListener('click', function () { show(!card.classList.contains('is-flipped'), true); });
});
back.addEventListener('keydown', function (e) { if (e.key === 'Escape') show(false, true); });
card.classList.add('is-ready');
show(false, false);
});
/* Flip profile cards on Bootstrap .card, .badge and .btn: two faces stacked in one grid cell. A button on each face turns the card (Y axis, or X with .fp-x);
the hidden face is made inert so it can't be tabbed into, focus moves to the face that comes up, and Escape turns it back.
Without JavaScript the faces simply stack. Under prefers-reduced-motion the turn becomes a crossfade. */
.fp{--fp-accent:#0369a1;--fp-ink:#0f172a;--fp-muted:#475569;--fp-line:#e2e8f0;--fp-back:#0f172a;--fp-back-ink:#e2e8f0;--fp-back-muted:#a5b4c8;--fp-cover:linear-gradient(120deg,#38bdf8,#6366f1);
height:100%;perspective:1400px;color:var(--fp-ink)}
.fp :where(h3,p,ul,dl,dd,dt){margin:0}
.fp .card{--bs-card-border-radius:22px;--bs-card-border-color:var(--fp-line);--bs-card-color:var(--fp-ink)}
.fp-inner{display:grid;gap:16px;height:100%}
.fp-face{position:relative;display:flex;flex-direction:column;border-radius:22px;overflow:hidden}
.fp-face:focus{outline:none}
.fp-face:focus-visible{outline:3px solid var(--fp-accent);outline-offset:3px}
.fp-front{box-shadow:0 24px 40px -30px rgba(15,23,42,.45);text-align:center}
.fp-cover{height:86px;background:var(--fp-cover)}
.fp-av{display:block;width:96px;height:96px;margin:-48px auto 0;border-radius:50%;overflow:hidden;border:4px solid #fff;box-sizing:content-box}
.fp-av svg{display:block;width:100%;height:100%}
.fp-body{display:flex;flex-direction:column;align-items:center;flex:1;padding:12px 22px 22px}
.fp .fp-name{font-size:1.15rem;font-weight:800;letter-spacing:-.01em;line-height:normal}
.fp-role{margin-top:4px;font-size:.9rem;color:var(--fp-muted)}
.fp-tags{display:flex;flex-wrap:wrap;justify-content:center;gap:6px;list-style:none;margin:16px 0 20px}
.fp-tags .badge{--bs-badge-padding-x:10px;--bs-badge-padding-y:4px;--bs-badge-font-size:.78rem;--bs-badge-font-weight:600;--bs-badge-color:#334155;background:#f1f5f9;line-height:normal}
.fp .fp-turn{--bs-btn-padding-x:16px;--bs-btn-padding-y:10px;--bs-btn-font-size:.88rem;--bs-btn-font-weight:700;--bs-btn-line-height:normal;--bs-btn-border-width:1.5px;--bs-btn-border-radius:12px;
--bs-btn-color:var(--fp-accent);--bs-btn-border-color:var(--fp-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--fp-accent);--bs-btn-hover-border-color:var(--fp-accent);
--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--fp-accent);--bs-btn-active-border-color:var(--fp-accent);--bs-btn-focus-box-shadow:none;
align-items:center;justify-content:center;gap:8px;margin-top:auto;transition:background-color .2s,color .2s}
.fp-turn svg{width:16px;height:16px}
.fp-turn:focus-visible{outline:3px solid var(--fp-accent);outline-offset:3px}
.fp .fp-back{--bs-card-bg:var(--fp-back);--bs-card-border-width:0;padding:24px 22px 22px;color:var(--fp-back-ink);box-shadow:0 24px 40px -26px rgba(15,23,42,.6);text-align:left}
.fp-k{font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--fp-back-muted)}
.fp-back .fp-name{margin-top:6px;color:#fff}
.fp-bio{margin-top:10px;font-size:.9rem;line-height:1.55}
.fp-facts{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;margin-top:16px;font-size:.84rem}
.fp-facts dt{color:var(--fp-back-muted)}
.fp-facts dt{font-weight:400}
.fp-facts dd{font-weight:600}
.fp-links{display:flex;gap:8px;list-style:none;margin:18px 0 20px}
.fp-links a{display:inline-flex;align-items:center;gap:6px;padding:7px 11px;border-radius:9px;background:rgba(255,255,255,.08);color:#fff;font-size:.82rem;font-weight:600;text-decoration:none}
.fp-links a:hover{background:rgba(255,255,255,.16);color:#fff}
.fp-links a:focus-visible{outline:3px solid #7dd3fc;outline-offset:2px}
.fp-links svg{width:15px;height:15px}
.fp .fp-back .fp-turn{--bs-btn-color:#7dd3fc;--bs-btn-border-color:#7dd3fc;--bs-btn-hover-color:var(--fp-back);--bs-btn-hover-bg:#7dd3fc;--bs-btn-hover-border-color:#7dd3fc;--bs-btn-active-color:var(--fp-back);--bs-btn-active-bg:#7dd3fc;--bs-btn-active-border-color:#7dd3fc;align-self:flex-start}
.fp-back .fp-turn:focus-visible{outline-color:#7dd3fc}
/* the turn, once the script has marked the card ready */
.fp .fp-turn{display:none}
.fp.is-ready .fp-turn{display:inline-flex}
.fp.is-ready .fp-inner{gap:0;transform-style:preserve-3d;transition:transform .7s cubic-bezier(.2,.7,.2,1)}
.fp.is-ready .fp-face{grid-area:1/1;-webkit-backface-visibility:hidden;backface-visibility:hidden}
.fp.is-ready .fp-back{transform:rotateY(180deg)}
.fp.is-flipped .fp-inner{transform:rotateY(180deg)}
.fp-x.is-ready .fp-back{transform:rotateX(180deg)}
.fp-x.is-flipped .fp-inner{transform:rotateX(180deg)}
/* colour themes */
.fp.t-rose{--fp-accent:#be123c;--fp-cover:linear-gradient(120deg,#fb7185,#f59e0b)}
.fp.t-teal{--fp-accent:#0f766e;--fp-cover:linear-gradient(120deg,#2dd4bf,#3b82f6)}
/* landscape speaker card */
.fp-x .fp-front.card{display:grid;grid-template-columns:auto 1fr;align-items:center;align-content:center;gap:6px 22px;padding:24px;text-align:left}
.fp-x .fp-av{grid-row:span 4;margin:0;border:0;width:104px;height:104px;border-radius:26px}
.fp-x .fp-body{display:contents}
.fp-x .fp-name{font-size:1.25rem}
.fp-talk{grid-column:2;font-size:.95rem;font-weight:600;line-height:1.4}
.fp-x .fp-front .fp-turn{grid-column:2;justify-self:start;margin-top:8px}
.fp-x .fp-back.card{display:grid;grid-template-columns:1fr auto;align-items:end;gap:16px 24px;padding:24px}
.fp-x .fp-facts{margin-top:12px}
.fp-x .fp-links{margin:0;flex-direction:column}
.fp-x .fp-back .fp-turn{grid-column:1/-1;justify-self:start}
@media (max-width:520px){.fp-x .fp-front.card,.fp-x .fp-back.card{grid-template-columns:1fr}.fp-x .fp-av{grid-row:auto}.fp-x .fp-front>*,.fp-talk,.fp-x .fp-front .fp-turn{grid-column:1}}
@media (prefers-reduced-motion:reduce){
.fp .fp-turn{transition:none}
.fp.is-ready .fp-inner,.fp.is-flipped .fp-inner{transform:none;transition:none}
.fp.is-ready .fp-face{-webkit-backface-visibility:visible;backface-visibility:visible;transform:none;transition:opacity .25s,visibility 0s .25s}
.fp.is-ready:not(.is-flipped) .fp-front,.fp.is-flipped .fp-back{transition:opacity .25s,visibility 0s}
.fp.is-ready .fp-back,.fp.is-flipped .fp-front{opacity:0;visibility:hidden}
.fp.is-flipped .fp-back{opacity:1;visibility:visible}
}
JavaScript
/* Flip cards: each .fp-turn turns its card over. The face that is turned away gets the inert attribute (no focus, hidden from
screen readers). Turning to the back moves focus to the back face so it is read from the top; turning to the front (with its
button or Escape) returns focus to the button that opened it. */
document.querySelectorAll('.fp').forEach(function (card) {
var front = card.querySelector('.fp-front'), back = card.querySelector('.fp-back');
var opener = front.querySelector('.fp-turn');
function show(flipped, moveFocus) {
card.classList.toggle('is-flipped', flipped);
front.inert = flipped; back.inert = !flipped;
opener.setAttribute('aria-expanded', String(flipped));
if (moveFocus) (flipped ? back : opener).focus({ preventScroll: true });
}
card.querySelectorAll('.fp-turn').forEach(function (btn) {
btn.addEventListener('click', function () { show(!card.classList.contains('is-flipped'), true); });
});
back.addEventListener('keydown', function (e) { if (e.key === 'Escape') show(false, true); });
card.classList.add('is-ready');
show(false, false);
});
/* The turn itself: once the script adds .is-ready the faces share one grid cell and hide their backs; .is-flipped rotates
the card (Y axis, or X axis with .fp-x). Under prefers-reduced-motion the turn becomes a crossfade. */
.fp-turn{display:none}
.fp.is-ready .fp-turn{display:inline-flex}
.fp.is-ready .fp-inner{gap:0;transform-style:preserve-3d;transition:transform .7s cubic-bezier(.2,.7,.2,1)}
.fp.is-ready .fp-face{grid-area:1/1;-webkit-backface-visibility:hidden;backface-visibility:hidden}
.fp.is-ready .fp-back{transform:rotateY(180deg)}
.fp.is-flipped .fp-inner{transform:rotateY(180deg)}
.fp-x.is-ready .fp-back{transform:rotateX(180deg)}
.fp-x.is-flipped .fp-inner{transform:rotateX(180deg)}
@media (prefers-reduced-motion:reduce){
.fp.is-ready .fp-inner,.fp.is-flipped .fp-inner{transform:none;transition:none}
.fp.is-ready .fp-face{-webkit-backface-visibility:visible;backface-visibility:visible;transform:none;transition:opacity .25s,visibility 0s .25s}
.fp.is-ready:not(.is-flipped) .fp-front,.fp.is-flipped .fp-back{transition:opacity .25s,visibility 0s}
.fp.is-ready .fp-back,.fp.is-flipped .fp-front{opacity:0;visibility:hidden}
.fp.is-flipped .fp-back{opacity:1;visibility:visible}
}
JavaScript
/* Flip cards: each .fp-turn turns its card over. The face that is turned away gets the inert attribute (no focus, hidden from
screen readers). Turning to the back moves focus to the back face so it is read from the top; turning to the front (with its
button or Escape) returns focus to the button that opened it. */
document.querySelectorAll('.fp').forEach(function (card) {
var front = card.querySelector('.fp-front'), back = card.querySelector('.fp-back');
var opener = front.querySelector('.fp-turn');
function show(flipped, moveFocus) {
card.classList.toggle('is-flipped', flipped);
front.inert = flipped; back.inert = !flipped;
opener.setAttribute('aria-expanded', String(flipped));
if (moveFocus) (flipped ? back : opener).focus({ preventScroll: true });
}
card.querySelectorAll('.fp-turn').forEach(function (btn) {
btn.addEventListener('click', function () { show(!card.classList.contains('is-flipped'), true); });
});
back.addEventListener('keydown', function (e) { if (e.key === 'Escape') show(false, true); });
card.classList.add('is-ready');
show(false, false);
});
About this set
Profile cards with a second side for the longer story. The portrait cards that turn on the Y axis show an illustrated avatar, the name, the role and three speciality tags on the front, and a dark back with a bio, three facts and Email and Portfolio links. The landscape speaker card turns on the X axis, from a speaker’s name and talk title to the session time, room, track and links to the calendar entry and slides. Unlike hover flip cards, these turn only when a button is pressed, Read bio on the front and Back to front on the back, so they work on touch screens and with a keyboard. The two faces share one grid cell, so the card is as tall as its longer side and never clips the back. A short script makes the hidden face inert, moves focus to the back when it comes up, returns focus to Read bio when the card turns back, and lets Escape turn it back. Without JavaScript the faces simply stack. Under prefers-reduced-motion the turn becomes a quick crossfade.
What’s included
Turns on a button press, never on hover alone
Hidden face is inert, so focus never lands on invisible links
Escape on the back turns the card to the front
Crossfade instead of a 3D turn under reduced motion
Y-axis portrait cards and an X-axis landscape speaker card
Accessibility
The turn buttons are real buttons with the person's name in hidden text, and the front button reports aria-expanded. The face turned away carries the inert attribute, so it is skipped by Tab and by screen readers; the back face receives focus when it appears and Escape returns to the front. Buttons and links show a 3px focus outline, and reduced motion replaces the rotation with a fade.
Customise it
Each card reads --fp-accent and --fp-cover (the .t-rose and .t-teal classes are examples), and the back uses --fp-back, --fp-back-ink and --fp-back-muted. Change the 0.7s duration on .fp-inner for a faster turn. In Tailwind the turn rules sit in a short style block and colours are utilities.