<!--
Animated Logo Columns
Accessibility checklist:
- Section heading id → aria-labelledby on <p-animated-logo-columns>
- Company list id → aria-describedby on <p-animated-logo-columns> (must match)
- List item text must match each logo's alt attribute exactly
-->
<section class="section animatedLogoColumns -center">
<div class="container">
<h2 id="animated-logo-columns-heading" class="animatedLogoColumns__title">Animated Logo Columns</h2>
<p class="animatedLogoColumns__description">Base layout for 12 or fewer logos. Each column alternates between two logos.</p>
<!-- aria-describedby is used during flipper mode only to point to the company list -->
<p-animated-logo-columns aria-labelledby="animated-logo-columns-heading" aria-describedby="animated-logo-columns-companies">
<!--
Text fallback for assistive tech while logos animate.
id must match aria-describedby on <p-animated-logo-columns>.
-->
<template #company-list>
<ul id="animated-logo-columns-companies" class="animatedLogoColumns__companyList srOnly">
<li>Company 1</li>
<li>Company 2</li>
<li>Company 3</li>
<li>Company 4</li>
<li>Company 5</li>
<li>Company 6</li>
<li>Company 7</li>
<li>Company 8</li>
<li>Company 9</li>
<li>Company 10</li>
<li>Company 11</li>
<li>Company 12</li>
</ul>
</template>
<div class="animatedLogoColumns__grid" style="--num-logos: 12">
<!-- Each column holds two logos that alternate via CSS animation. -->
<div class="animatedLogoColumns__column" style="--column-id: 0">
<div class="animatedLogoColumns__logoWrapper">
<!-- alt text must show company name for accessibility -->
<img class="animatedLogoColumns__logo" alt="Company 1" src="https://placehold.co/260x150/9933ff/FFF?text=Company+1" />
</div>
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 2" src="https://placehold.co/260x150/9933ff/FFF?text=Company+2" />
</div>
</div>
<div class="animatedLogoColumns__column" style="--column-id: 1">
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 3" src="https://placehold.co/260x150/9933ff/FFF?text=Company+3" />
</div>
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 4" src="https://placehold.co/260x150/9933ff/FFF?text=Company+4" />
</div>
</div>
<div class="animatedLogoColumns__column" style="--column-id: 2">
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 5" src="https://placehold.co/260x150/9933ff/FFF?text=Company+5" />
</div>
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 6" src="https://placehold.co/260x150/9933ff/FFF?text=Company+6" />
</div>
</div>
<div class="animatedLogoColumns__column" style="--column-id: 3">
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 7" src="https://placehold.co/260x150/9933ff/FFF?text=Company+7" />
</div>
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 8" src="https://placehold.co/260x150/9933ff/FFF?text=Company+8" />
</div>
</div>
<div class="animatedLogoColumns__column" style="--column-id: 4">
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 9" src="https://placehold.co/260x150/9933ff/FFF?text=Company+9" />
</div>
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 10" src="https://placehold.co/260x150/9933ff/FFF?text=Company+10" />
</div>
</div>
<div class="animatedLogoColumns__column" style="--column-id: 5">
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 11" src="https://placehold.co/260x150/9933ff/FFF?text=Company+11" />
</div>
<div class="animatedLogoColumns__logoWrapper">
<img class="animatedLogoColumns__logo" alt="Company 12" src="https://placehold.co/260x150/9933ff/FFF?text=Company+12" />
</div>
</div>
</div>
</p-animated-logo-columns>
</div>
</section>
@use '@/core' as *;
// Animated logo columns: CSS-driven flip animation with a static reduced-motion fallback.
// Accessibility toggling (aria-hidden, play/pause) lives in PAnimatedLogoColumns.vue.
.animatedLogoColumns {
$b: &;
&.-center {
#{$b}__title,
#{$b}__description {
text-align: center;
}
}
&__title,
&__description {
margin-bottom: 0;
}
&__wrapper {
position: relative;
padding: 2rem 0;
// Paused via JS (play button or focus). Stops CSS animations without removing them.
&.-paused #{$b}__logoWrapper {
animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
padding-top: 2rem;
#{$b}__playPause.button {
display: none;
}
}
}
&__playPause.button.-circle {
position: absolute;
top: 1rem;
left: 1rem;
z-index: 2;
width: 1.5rem;
height: 1.5rem;
}
// Text-only company list for assistive tech while logos animate.
// List names must match each img alt attribute (see animatedLogoColumns.html).
&__companyList {
list-style: none;
margin: 0;
padding: 0;
}
&__grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@include at(md) {
grid-template-columns: repeat(auto-fit, minmax(115px, 1fr));
}
}
// Fixed-height viewport; two logo wrappers stack and animate inside.
&__column {
width: 100%;
position: relative;
overflow: clip;
height: 150px;
max-width: 200px;
margin: auto;
}
&__logoWrapper {
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
display: flex;
animation: 9s ease-in-out infinite logo-slide;
padding: 1.5rem;
position: relative;
isolation: isolate;
// Hover glow behind the logo.
&::before {
content: '';
display: block;
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
opacity: 0;
transition: opacity var(--root-duration-moderate) var(--root-ease-out);
background-color: transparent;
background-image: radial-gradient(ellipse 40% 60% at 50% 50%, rgb(153 51 255 / 0.35) 0%, rgb(153 51 255 / 0.22) 35%, rgb(153 51 255 / 0.12) 58%, rgb(153 51 255 / 0.04) 82%, rgb(153 51 255 / 0) 100%);
background-repeat: no-repeat;
background-position: center center;
background-size: 100% 100%;
width: 100%;
height: 100%;
}
&:hover {
&::before {
opacity: 1;
}
#{$b}__logo {
opacity: 1;
}
}
// Stagger each column's animation start.
&:where(:first-child) {
animation-delay: calc(var(--column-id) * 0.15s);
}
// Second logo in each column: positioned below, runs the reverse keyframe.
&:where(:last-child):not(:only-child) {
inset: calc(var(--spacing, 0.25rem) * 0);
animation-name: logo-slide-reverse;
animation-delay: calc(var(--column-id) * 0.15s);
position: absolute;
transform: translateY(100%);
}
}
&__logo {
width: 100%;
height: 100%;
object-fit: contain;
position: relative;
z-index: 1;
opacity: 0.75;
transition: opacity var(--root-duration-moderate) var(--root-ease-out);
place-content: center;
img {
object-fit: contain;
width: 100%;
max-height: 100%;
}
}
// First logo in a column: visible, slides up and out at the midpoint.
@keyframes logo-slide {
0% {
opacity: 1;
transform: translateY(0);
}
46% {
opacity: 1;
transform: translateY(0);
}
50% {
opacity: 0;
transform: translateY(-100%);
}
51% {
opacity: 0;
transform: translateY(100%);
}
96% {
opacity: 0;
transform: translateY(100%);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
// Second logo in a column: hidden below, slides in at the midpoint.
@keyframes logo-slide-reverse {
0% {
opacity: 0;
transform: translateY(100%);
}
46% {
opacity: 0;
transform: translateY(100%);
}
50% {
opacity: 1;
transform: translateY(0);
}
96% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-100%);
}
}
// Static grid fallback when the user prefers reduced motion.
// Flattens columns so every logo is visible at once (matches img alt text in AT).
@media (prefers-reduced-motion: reduce) {
#{$b}__grid {
grid-template-columns: repeat(2, 1fr);
@include at(md) {
grid-template-columns: repeat(calc(var(--num-logos) / 2), minmax(115px, 1fr));
}
}
// Remove column stacking; each logo wrapper becomes a direct grid cell.
#{$b}__column {
display: contents;
height: auto;
max-width: none;
overflow: visible;
}
#{$b}__logoWrapper {
animation: none;
height: fluid-rems(5, 7.5);
inset: auto;
isolation: auto;
margin: auto;
max-width: 150px;
opacity: 1;
position: static;
transform: none;
&::before {
display: none;
}
}
#{$b}__logo {
opacity: 1;
}
}
}