Mira Holt
Prints, posters and slow animation.
- Risograph test, third pass
- Poster for the night market
- Type study: grotesques
- Zine cover, rejected
- Sketchbook, March
Mira Holt
Prints, posters and slow animation.
import { useState } from 'react';
const POSTS = ['Risograph test, third pass', 'Poster for the night market', 'Type study: grotesques', 'Zine cover, rejected', 'Sketchbook, March'];
const css = `
.card-header { position: relative; width: 240px; height: 210px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); overflow: hidden; }
.card-header__scroll { position: absolute; inset: 0; overflow-y: auto; border-radius: inherit; scrollbar-width: none; }
.card-header__scroll::-webkit-scrollbar { display: none; }
.card-header__scroll:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: -2px; }
.card-header__content { padding: 112px 14px 14px; }
.card-header__cover {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: 1;
background: repeating-linear-gradient(135deg, light-dark(#dbdde1, #2e3035) 0 9px, light-dark(#cbced3, #393c41) 9px 10px);
box-shadow: 0 6px 12px -8px var(--card-header-shadow);
pointer-events: none;
}
.card-header { --card-header-shadow: light-dark(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.7)); }
.card-header__avatar {
position: absolute;
left: 0;
top: 0;
z-index: 2;
display: grid;
place-items: center;
box-sizing: border-box;
width: 52px;
height: 52px;
border: 3px solid light-dark(#f3f4f7, #1a1c1f);
border-radius: 50%;
background: light-dark(#5f626a, #a7abb2);
color: light-dark(#fbfcfd, #121417);
font-size: 15px;
font-weight: 700;
transform-origin: 0 0;
pointer-events: none;
}
.card-header__bar { position: absolute; left: 44px; top: 12px; z-index: 2; margin: 0; font-size: 12px; font-weight: 600; color: light-dark(#373a40, #f1f2f3); pointer-events: none; }
.card-header__name { margin: 0; font-size: 15px; font-weight: 700; color: light-dark(#373a40, #f1f2f3); }
.card-header__bio { margin: 2px 0 12px; font-size: 11.5px; color: light-dark(#5f626a, #a7abb2); }
.card-header__posts { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.card-header__post { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: light-dark(#42454c, #e5e6e9); }
.card-header__thumb { flex: none; width: 32px; height: 32px; border-radius: 6px; background: light-dark(#dbdde1, #2e3035); }
.card-header__post:nth-child(even) .card-header__thumb { background: light-dark(#cbced3, #393c41); }
`;
export function HeaderCard() {
const [p, setP] = useState(0);
// Collapse over the first 70px of scroll: the cover shortens, the avatar shrinks up into the bar and the name fades in beside it.
const name = Math.max(0, (p - 0.6) / 0.4);
return (
<>
<style>
{css}
</style>
<article className="card-header">
<div className="card-header__scroll" tabIndex={0} aria-label="Mira Holt, profile" onScroll={(e) => setP(Math.min(1, e.currentTarget.scrollTop / 70))}>
<div className="card-header__content">
<p className="card-header__name">Mira Holt</p>
<p className="card-header__bio">Prints, posters and slow animation.</p>
<ul className="card-header__posts">
{POSTS.map((t) => (
<li key={t} className="card-header__post">
<span className="card-header__thumb" aria-hidden="true" />
{t}
</li>
))}
</ul>
</div>
</div>
<span className="card-header__cover" style={{ height: 84 - 44 * p }} aria-hidden="true" />
<span className="card-header__avatar" style={{ transform: `translate(${14 - 4 * p}px, ${58 - 51 * p}px) scale(${1 - 0.5 * p})` }} aria-hidden="true">
MH
</span>
<p className="card-header__bar" style={{ opacity: name, transform: `translateY(${(1 - name) * 6}px)` }} aria-hidden="true">
Mira Holt
</p>
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardHeader(el) on the mounted element instead.
<article class="card-header">
<div class="card-header__scroll" tabindex="0" aria-label="Mira Holt, profile">
<div class="card-header__content">
<p class="card-header__name">Mira Holt</p>
<p class="card-header__bio">Prints, posters and slow animation.</p>
<ul class="card-header__posts">
<li class="card-header__post">
<span class="card-header__thumb" aria-hidden="true"></span>
Risograph test, third pass
</li>
<li class="card-header__post">
<span class="card-header__thumb" aria-hidden="true"></span>
Poster for the night market
</li>
<li class="card-header__post">
<span class="card-header__thumb" aria-hidden="true"></span>
Type study: grotesques
</li>
<li class="card-header__post">
<span class="card-header__thumb" aria-hidden="true"></span>
Zine cover, rejected
</li>
<li class="card-header__post">
<span class="card-header__thumb" aria-hidden="true"></span>
Sketchbook, March
</li>
</ul>
</div>
</div>
<span class="card-header__cover" style="height:84px" aria-hidden="true"></span>
<span class="card-header__avatar" style="transform:translate(14px, 58px) scale(1)" aria-hidden="true">MH</span>
<p class="card-header__bar" style="opacity:0;transform:translateY(6px)" aria-hidden="true">Mira Holt</p>
</article>.card-header { position: relative; width: 240px; height: 210px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); overflow: hidden; }
.card-header__scroll { position: absolute; inset: 0; overflow-y: auto; border-radius: inherit; scrollbar-width: none; }
.card-header__scroll::-webkit-scrollbar { display: none; }
.card-header__scroll:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: -2px; }
.card-header__content { padding: 112px 14px 14px; }
.card-header__cover {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: 1;
background: repeating-linear-gradient(135deg, light-dark(#dbdde1, #2e3035) 0 9px, light-dark(#cbced3, #393c41) 9px 10px);
box-shadow: 0 6px 12px -8px var(--card-header-shadow);
pointer-events: none;
}
.card-header { --card-header-shadow: light-dark(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.7)); }
.card-header__avatar {
position: absolute;
left: 0;
top: 0;
z-index: 2;
display: grid;
place-items: center;
box-sizing: border-box;
width: 52px;
height: 52px;
border: 3px solid light-dark(#f3f4f7, #1a1c1f);
border-radius: 50%;
background: light-dark(#5f626a, #a7abb2);
color: light-dark(#fbfcfd, #121417);
font-size: 15px;
font-weight: 700;
transform-origin: 0 0;
pointer-events: none;
}
.card-header__bar { position: absolute; left: 44px; top: 12px; z-index: 2; margin: 0; font-size: 12px; font-weight: 600; color: light-dark(#373a40, #f1f2f3); pointer-events: none; }
.card-header__name { margin: 0; font-size: 15px; font-weight: 700; color: light-dark(#373a40, #f1f2f3); }
.card-header__bio { margin: 2px 0 12px; font-size: 11.5px; color: light-dark(#5f626a, #a7abb2); }
.card-header__posts { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
.card-header__post { display: flex; align-items: center; gap: 10px; font-size: 11.5px; color: light-dark(#42454c, #e5e6e9); }
.card-header__thumb { flex: none; width: 32px; height: 32px; border-radius: 6px; background: light-dark(#dbdde1, #2e3035); }
.card-header__post:nth-child(even) .card-header__thumb { background: light-dark(#cbced3, #393c41); }function initCardHeader(root) {
var scroll = root.querySelector('.card-header__scroll');
var cover = root.querySelector('.card-header__cover');
var avatar = root.querySelector('.card-header__avatar');
var bar = root.querySelector('.card-header__bar');
// Collapse over the first 70px of scroll: the cover shortens, the avatar shrinks up into the bar and the name fades in beside it.
function onScroll() {
var p = Math.min(1, scroll.scrollTop / 70);
var name = Math.max(0, (p - 0.6) / 0.4);
cover.style.height = 84 - 44 * p + 'px';
avatar.style.transform = 'translate(' + (14 - 4 * p) + 'px, ' + (58 - 51 * p) + 'px) scale(' + (1 - 0.5 * p) + ')';
bar.style.opacity = name;
bar.style.transform = 'translateY(' + (1 - name) * 6 + 'px)';
}
scroll.addEventListener('scroll', onScroll, { passive: true });
return function () {
scroll.removeEventListener('scroll', onScroll);
};
}
document.querySelectorAll('.card-header').forEach(initCardHeader);Goal: Create a "Header" card.
Visual behavior: A profile card that scrolls inside itself: as you scroll the posts, the cover shrinks into a slim header and the avatar travels up into it, shrinking, while the name fades in beside it.
Technique:
- Scroll progress over the first 70px drives everything directly: the cover's height, the avatar's translate and scale and the bar name's opacity, so it tracks the scroll exactly in both directions.
- The cover and the avatar sit outside the scrolling layer, on top of it, with pointer events off, so the list scrolls under them.
- The name in the bar fades in only over the last part of the collapse, once the avatar has arrived.
Accessibility: The scrolling area is focusable and labelled, so it scrolls from the keyboard. The collapsing header is aria-hidden; the real name and bio are in the content. It moves only as far as the reader scrolls, so prefers-reduced-motion needs nothing more.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Header" card.
Visual behavior: A profile card that scrolls inside itself: as you scroll the posts, the cover shrinks into a slim header and the avatar travels up into it, shrinking, while the name fades in beside it.
Technique:
- Scroll progress over the first 70px drives everything directly: the cover's height, the avatar's translate and scale and the bar name's opacity, so it tracks the scroll exactly in both directions.
- The cover and the avatar sit outside the scrolling layer, on top of it, with pointer events off, so the list scrolls under them.
- The name in the bar fades in only over the last part of the collapse, once the avatar has arrived.
Accessibility: The scrolling area is focusable and labelled, so it scrolls from the keyboard. The collapsing header is aria-hidden; the real name and bio are in the content. It moves only as far as the reader scrolls, so prefers-reduced-motion needs nothing more.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
A profile card that scrolls inside itself: as you scroll the posts, the cover shrinks into a slim header and the avatar travels up into it, shrinking, while the name fades in beside it.
The scrolling area is focusable and labelled, so it scrolls from the keyboard. The collapsing header is aria-hidden; the real name and bio are in the content. It moves only as far as the reader scrolls, so prefers-reduced-motion needs nothing more.