GODRICH

9 Media Page Templates — Copy-Paste

A media page template bundles the sections a press room needs — release feed, downloadable assets, spokesperson contact — and one of the nine below copies a

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine aren't sorted by popularity — they follow the order a reporter or investor actually scrolls a press page. A hero states who you are, a release list proves you're active, a media kit hands over assets a journalist can use without asking, a mentions bar shows where you've already landed, a stat block backs that up with numbers, a contact card removes the excuse not to reach out, a founder quote adds a human voice, a timeline shows the climb that got here, and a footer keeps the relationship going after the visit ends. Every avatar circle, logo block, and quote mark below is a shape drawn from this site's own $subject-ink, $subject-blue, and $subject-cream tokens — none of it is a real photo or a real press logo, and swapping in either later doesn't touch the layout or the JavaScript underneath.

01Press hero

Three blurred circles drift behind the headline on a continuous translate-and-scale loop that never stops, while a rotated-square wordmark placeholder, headline, subhead, and two CTAs hold their position in front. The right-hand "subscribe to releases" button actually flips aria-pressed and fills its bookmark icon on every real click, which is what a company press room or a media hub's top section needs.

transformopacityaria-pressed
.ph__orb--1 { animation: ph-float-a 2s ease-in-out infinite; }
@keyframes ph-float-a {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate($sp-10, -$sp-8) scale(1.5); }
}
.ph__sub-btn[aria-pressed="true"] {
  background: rgba(255, 247, 230, .12);
  border-color: $subject-cream;
}
.ph__sub-btn[aria-pressed="true"] .ph__sub-ic {
  opacity: 1;
  color: $color;
  transform: scale(1.15);
}

02Press release list

Clicking a category chip (all, product, partnership, award) actually folds every row that doesn't match with a max-height and opacity transition rather than just dimming it, and the filter is exclusive — only one chip can read aria-pressed="true" at a time. It's built for a press room's release archive or a newsroom index above the fold.

clickfilteraria-pressed
chips.forEach(function (chip) {
  chip.addEventListener('click', function () {
    root.classList.remove('is-demo');
    chips.forEach(function (c) { c.setAttribute('aria-pressed', 'false'); });
    chip.setAttribute('aria-pressed', 'true');
    var cat = chip.getAttribute('data-cat');
    rows.forEach(function (row) {
      var show = cat === 'all' || row.getAttribute('data-cat') === cat;
      row.classList.toggle('is-hidden', !show);
    });
  });
});

03Media kit download

Checking any of the three files — logo pack, brand guide, product screenshots — actually enables the download button and appends the count to its label; clicking it draws a checkmark onto each selected row with stroke-dashoffset rather than just swapping an icon. It suits a media kit section or a brand resource page where a journalist needs to grab more than one asset at once.

checkboxstroke-dashoffsetclick
.mk::before {
  content: "";
  position: absolute; left: 0; right: 0; top: 0;
  height: 3px;
  background: $color;
  animation: mk-scan 2s $easing infinite;
}
.mk__done-ic {
  stroke: $color;
  stroke-width: 2.5;
  stroke-dasharray: 20;
  stroke-dashoffset: 20;
  opacity: 0;
  transition: stroke-dashoffset $duration $easing, opacity $duration $easing;
}
.mk__row.is-done .mk__done-ic { stroke-dashoffset: 0; opacity: 1; }
.mk__btn:disabled { opacity: .4; cursor: not-allowed; }

04Press mentions bar

Eight placeholder outlet marks — flat shapes, not real logos — sit in a track duplicated end to end so a translateX(-50%) loop scrolls with no visible seam, and hovering the strip or tapping the pause button actually freezes it in place. This is the standard spot for an "as seen in" row or a partner logo bar under a hero.

translateX(-50%)animation-play-statepointerenter
// the track skips gap entirely — each logo gets margin-right instead
.mb__track { display: flex; width: max-content; animation: mb-scroll $duration $easing infinite; }
.mb.is-paused .mb__track,
.mb__viewport:hover .mb__track { animation-play-state: paused; }
.mb__logo { flex: 0 0 auto; width: 64px; height: 32px; margin-right: $sp-6; }
@keyframes mb-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

05Stat counter

Three cards' big numbers climb from zero to their real targets — 128, 42, 3,200 — the instant the page loads, run through an eased curve rather than a linear tick, and land exactly on the target with no overshoot. The digits carry font-variant-numeric: tabular-nums so they don't jitter in width while counting, and this fits an "our numbers" block on an about page or a press page's credibility strip.

requestAnimationFramecounteropacity
.sc__card::before {
  content: "";
  position: absolute; left: 0; right: 0; top: 0;
  height: 3px;
  background: $color;
  animation: sc-pulse 2s $easing infinite;
}
.sc__card:nth-child(2)::before { animation-delay: 220ms; }
.sc__num {
  font-weight: 800; font-size: clamp(20px, 7vw, 28px); line-height: 1; font-family: inherit;
  color: $subject-cream;
  font-variant-numeric: tabular-nums;
}

06Media contact card

Clicking the email button actually writes the address to the clipboard through navigator.clipboard.writeText(), then draws a checkmark onto the button with stroke-dashoffset before reverting the label 1.6 seconds later. It's built for the "reach our PR team" card that sits at the bottom of a press room or an interview-request section.

clipboardstroke-dashoffsetclick
.mc__orb--1 { background: radial-gradient(circle, $subject-blue, transparent 68%); animation: mc-drift 8s ease-in-out infinite; }
.mc__orb--2 { background: radial-gradient(circle, $subject-ink, transparent 68%); animation: mc-drift 7s ease-in-out infinite reverse; }
.mc__avatar { background: $subject-ink; border-radius: 50%; }
.mc__copy-ic {
  stroke: $color;
  stroke-dasharray: 20;
  stroke-dashoffset: 20;
  opacity: 0;
  transition: stroke-dashoffset $duration $easing, opacity $duration $easing;
}
.mc__copy.is-copied .mc__copy-ic { stroke-dashoffset: 0; opacity: 1; }

07Founder quote

Two founder quotes trade places behind a pair of dots — clicking one runs a real opacity cross-fade between two absolutely-positioned slides, while a large quotation mark keeps wobbling with rotate and scale in the background the whole time. It's the founder or CEO message block that sits inside a press room or an about page's leadership section.

opacityclickaria-selected
dots.forEach(function (dot) {
  dot.addEventListener('click', function () {
    root.classList.remove('is-demo');
    var i = dot.getAttribute('data-i');
    dots.forEach(function (d) { d.classList.remove('is-active'); d.setAttribute('aria-selected', 'false'); });
    slides.forEach(function (s) { s.classList.remove('is-active'); });
    dot.classList.add('is-active'); dot.setAttribute('aria-selected', 'true');
    root.querySelector('.fq__slide[data-i="' + i + '"]').classList.add('is-active');
  });
});

08Milestone timeline

Clicking any of the four stops on the horizontal line actually fills the track up to that point with a real scaleX transform, keyed to the stop's position out of four, and swaps the paragraph above it to that year's text. The active dot keeps pulsing with a box-shadow ring the whole time it's selected, which makes this fit a company history section or a press room's "how we got here" block.

scaleXclickaria-current
.mt__fill {
  position: absolute; left: 0; right: 0; top: 4px;
  height: 3px;
  background: $color;
  transform: scaleX(.25);
  transform-origin: left;
  transition: transform $duration $easing;
}
.mt__stop[aria-current="step"] .mt__dot {
  background: $color;
  transform: scale(1.3);
  box-shadow: 0 0 0 4px rgba(47, 109, 246, .18);
}

09Press footer

Submitting the newsletter form runs a real regex check against the typed address rather than pretending to validate. A passing address draws a checkmark onto the submit button with stroke-dashoffset, while a failing one shakes the input left and right with translateX — the three link columns underneath stay put either way, which is what a press room or media page needs at the very bottom.

submitstroke-dashoffsetclick
.pf__input.is-shake { animation: pf-shake $dur-quick $ease-out; }
@keyframes pf-shake {
  0%, 100% { transform: translateX(0); }
  25%      { transform: translateX(-5px); }
  75%      { transform: translateX(5px); }
}
.pf__submit { animation: pf-glow 2s ease-in-out infinite; }
@keyframes pf-glow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(47, 109, 246, .35); }
  50%      { box-shadow: 0 0 0 6px rgba(47, 109, 246, 0); }
}
.pf__form.is-done .pf__check { stroke-dashoffset: 0; opacity: 1; }

Where it breaks — the trap

The real trap here wasn't an animation at all — it was a background color hiding a decoration sitting right behind it. The media contact card (06) originally had an opaque cream background, and the two drifting orbs behind it sit at z-index: -1, so they were rendering completely behind that opaque layer. Nothing looked broken on screen, but almost no pixels were actually changing frame to frame, which barely counted as motion at all. Removing the card's own background and letting the orbs show directly against the stage color fixed it — a reminder that z-index: -1 only works as a decoration if whatever sits above it is transparent.

A second, more mundane trap showed up in the release list (02) and the media kit (03) at the 320×200 breakpoint. Both fold rows open and shut with max-height, and without box-sizing: border-box on the folding element, the row's padding stacks on top of the height you set rather than inside it, so a max-height: 30px row with 8px of vertical padding actually renders at 38px. That extra height was invisible at the full 480×300 canvas but pushed the layout into a silent vertical scroll once the width dropped to 320px — adding box-sizing: border-box and trimming the padding inside a @media (max-width: 340px), (max-height: 220px) block cleared it. The mentions bar (04) sidesteps a related trap by design: give a duplicated marquee track a gap instead of margin-right on each item and the seam between the two copies comes up short by gap ÷ 2. Open the zip with 7a6mw6kt typed exactly as it appears here and you'll find every one of the nine already built the way this page describes, margin-right included.

Accessibility (reduced-motion)

All nine respect prefers-reduced-motion: reduce, and each one keeps its actual state rather than just freezing mid-animation — the release list stays filtered, the timeline's fill bar stays at whatever point it reached, and the stat counter keeps counting up since the number itself carries information, with only its accent pulse turning off. The media kit's checkboxes (03) are native input[type="checkbox"] elements, so Tab and Space operate them with no extra script, and its result text plus the contact card's copy hint (06) and the footer's submit result (09) all live in a role="status" region with aria-live="polite" so a screen reader announces the outcome the moment it happens. The timeline (08) updates a real aria-current="step" on the active stop, and the founder quote's dots (07) sit in a role="group" with aria-selected tracking which slide is showing. The clipboard copy in 06 follows MDN's Clipboard API reference and falls back to a hidden textarea plus execCommand('copy') in browsers that don't support it.

More layout sections and static templates live on the template category hub, and what this site does and doesn't sell is on the about page.

FAQ

Can I drop real logos and photos into these placeholders?

Yes — the avatar circle, wordmark, and all eight outlet marks are CSS shapes built from this site's own $subject-ink, $subject-blue, and $subject-cream tokens, not real image files, so replacing any of them with an actual <img> or logo file is a straight swap that leaves the layout and JavaScript untouched.

Does the media kit download actually send files, or is it just decorating the click?

The checkbox state, the button's enabled/disabled state, and the per-row checkmark are all real DOM state changes, not a fake animation loop. What it doesn't do is fetch a real file, since the demo has nothing to download — pointing each row at a real asset URL is the only change needed to make it functional.

How different is the React version from the plain CSS one?

The behavior matches. Which chip is active, which files are checked, which quote is showing, and which timeline stop is selected all live in useState instead of DOM attributes and classes, and the three SCSS variables — duration, easing, color — arrive as component props that get forwarded onto the section as CSS custom properties. The stat counter's requestAnimationFrame loop runs inside a useEffect with its own cleanup, so both versions count up in lockstep.

Enter the archive password

The password is inside this article. You will find it as you read.