Logo grid
Sponsor logos sized to equal weight, in color on hover.
Code
Copy into src/components/ph/ .
logo-grid.css
/* --logo-base sets the em unit the normalized logo sizes are written in. */
.ph-logo-grid {
--logo-base: 32px;
font-size: var(--logo-base);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.9em 1.2em;
}
@media (min-width: 768px) {
.ph-logo-grid {
--logo-base: 45px;
gap: 1em 1.75em;
}
}
.ph-logo-grid__cell {
position: relative;
display: block;
flex: none;
}
.ph-logo-grid__img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
/* Static silhouette. Tweening invert() toward none passes through flat gray,
so the brand copy crossfades in on top instead. */
.ph-logo-grid__img--wash {
filter: brightness(0);
}
.dark .ph-logo-grid__img--wash,
[data-tone="dark"] .ph-logo-grid__img--wash {
filter: brightness(0) invert(1);
}
.ph-logo-grid__img--brand {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.2s ease;
}
.ph-logo-grid__cell:is(:hover, :focus-visible) .ph-logo-grid__img--brand {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.ph-logo-grid__img--brand {
transition: none;
}
}
LogoGrid.astro
---
import "./logo-grid.css";
type Logo = {
name: string;
src: string;
href?: string;
/** Size normalized around a 48px base. Omit for a plain 0.75em-tall logo. */
width?: number;
height?: number;
/** Optical-centering nudge in em, e.g. "translate(0em, -0.02em)". */
transform?: string;
};
type Props = {
logos: Logo[];
tone?: "light" | "dark";
class?: string;
};
const BASE = 48;
const { logos, tone, class: className } = Astro.props;
---
<div data-tone={tone} class:list={["ph-logo-grid", className]}>
{
logos.map((l) => {
const Cell = l.href ? "a" : "div";
const size = l.width && l.height ? `width: ${l.width / BASE}em; height: ${l.height / BASE}em;` : "height: 0.75em; width: auto;";
return (
<Cell
class="ph-logo-grid__cell"
href={l.href}
target={l.href ? "_blank" : undefined}
rel={l.href ? "noopener noreferrer" : undefined}
style={`${size}${l.transform ? ` transform: ${l.transform};` : ""}`}
>
<img class="ph-logo-grid__img ph-logo-grid__img--wash" src={l.src} alt={l.name} width={l.width} height={l.height} loading="lazy" decoding="async" />
<img class="ph-logo-grid__img ph-logo-grid__img--brand" src={l.src} alt="" aria-hidden="true" width={l.width} height={l.height} loading="lazy" decoding="async" />
</Cell>
);
})
}
</div>
Copy into components/ph/ (or src/components/ph/).
logo-grid.css
/* --logo-base sets the em unit the normalized logo sizes are written in. */
.ph-logo-grid {
--logo-base: 32px;
font-size: var(--logo-base);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.9em 1.2em;
}
@media (min-width: 768px) {
.ph-logo-grid {
--logo-base: 45px;
gap: 1em 1.75em;
}
}
.ph-logo-grid__cell {
position: relative;
display: block;
flex: none;
}
.ph-logo-grid__img {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
/* Static silhouette. Tweening invert() toward none passes through flat gray,
so the brand copy crossfades in on top instead. */
.ph-logo-grid__img--wash {
filter: brightness(0);
}
.dark .ph-logo-grid__img--wash,
[data-tone="dark"] .ph-logo-grid__img--wash {
filter: brightness(0) invert(1);
}
.ph-logo-grid__img--brand {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 0.2s ease;
}
.ph-logo-grid__cell:is(:hover, :focus-visible) .ph-logo-grid__img--brand {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.ph-logo-grid__img--brand {
transition: none;
}
}
logo-grid.tsx
import type { CSSProperties } from "react";
import { cn } from "./cn";
import "./logo-grid.css";
type Logo = {
name: string;
src: string;
href?: string;
/** Size normalized around a 48px base. Omit for a plain 0.75em-tall logo. */
width?: number;
height?: number;
/** Optical-centering nudge in em, e.g. "translate(0em, -0.02em)". */
transform?: string;
};
type LogoGridProps = {
logos: Logo[];
tone?: "light" | "dark";
className?: string;
};
const BASE = 48;
export function LogoGrid({ logos, tone, className }: LogoGridProps) {
return (
<div data-tone={tone} className={cn("ph-logo-grid", className)}>
{logos.map((l) => {
const style: CSSProperties = {
...(l.width && l.height ? { width: `${l.width / BASE}em`, height: `${l.height / BASE}em` } : { height: "0.75em", width: "auto" }),
transform: l.transform,
};
const imgs = (
<>
<img className="ph-logo-grid__img ph-logo-grid__img--wash" src={l.src} alt={l.name} width={l.width} height={l.height} loading="lazy" decoding="async" />
<img className="ph-logo-grid__img ph-logo-grid__img--brand" src={l.src} alt="" aria-hidden="true" width={l.width} height={l.height} loading="lazy" decoding="async" />
</>
);
return l.href ? (
<a key={l.src} className="ph-logo-grid__cell" href={l.href} target="_blank" rel="noopener noreferrer" style={style}>
{imgs}
</a>
) : (
<div key={l.src} className="ph-logo-grid__cell" style={style}>
{imgs}
</div>
);
})}
</div>
);
}
cn.ts
export const cn = (...parts: (string | false | null | undefined)[]) => parts.filter(Boolean).join(" ");