The frame assembles in reading order: left markers, horizontal lines, right markers, vertical lines. Content settles last.
A framed container marked with a plus at each corner. The plus sits on a black patch that masks the hairlines crossing behind it, so the frame reads as four edges meeting four markers. It assembles in reading order on intro.
Calling out a single featured item, a pull quote, or a key spec. One per section, two on a page at most. The frame is the hierarchy, so do not nest frames or stack them in a grid.
Do not round the plus patch, it must stay opaque black to mask the lines. Do not animate lines with width, use scaleX and scaleY. Keep content last in the build order.
<!-- Requires GSAP: https://cdnjs.cloudflare.com/ajax/libs/gsap/3.13.0/gsap.min.js -->
<style>
:root{
--black:#050505;--surface:#0C0C0C;--white:#FFFFFF;--grey:#A3A3A3;--grey-dim:#6B6B6B;--accent:#22D3EE;
--line:rgba(255,255,255,.16);--line-faint:rgba(255,255,255,.07);
--font-display:'Space Grotesk',system-ui,sans-serif;--font-mono:'JetBrains Mono',ui-monospace,monospace;
--dur-fast:150ms;--ease-out:cubic-bezier(.16,1,.3,1);
}
*{margin:0;padding:0;box-sizing:border-box;border-radius:0!important}
body{background:var(--black);color:var(--white);font-family:var(--font-display);min-height:100vh;padding:64px 48px}
.plus-frame{position:relative;max-width:640px;margin:0 auto;padding:40px}
.pf-line{position:absolute;display:block;background:var(--line)}
.pf-top {top:0; left:0; width:100%;height:1px;transform-origin:left center}
.pf-bottom{bottom:0;left:0; width:100%;height:1px;transform-origin:left center}
.pf-left {top:0; left:0; width:1px; height:100%;transform-origin:center top}
.pf-right {top:0; right:0;width:1px; height:100%;transform-origin:center top}
.pf-plus{position:absolute;font-family:var(--font-mono);font-size:12px;line-height:1;color:var(--white);background:var(--black);padding:1px 2px;z-index:2}
.pf-tl{top:-7px;left:-5px}
.pf-tr{top:-7px;right:-5px}
.pf-bl{bottom:-7px;left:-5px}
.pf-br{bottom:-7px;right:-5px}
.pf-content .k{font-family:var(--font-mono);font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:var(--grey);display:block;margin-bottom:16px}
.pf-content h2{font-size:clamp(1.5rem,2.5vw,2rem);font-weight:500;letter-spacing:-.01em;line-height:1.25}
.pf-content p{color:var(--grey);font-size:.875rem;margin-top:16px;max-width:48ch}
</style>
<div class="plus-frame" id="frame">
<i class="pf-line pf-top"></i>
<i class="pf-line pf-bottom"></i>
<i class="pf-line pf-left"></i>
<i class="pf-line pf-right"></i>
<span class="pf-plus pf-tl">+</span>
<span class="pf-plus pf-tr">+</span>
<span class="pf-plus pf-bl">+</span>
<span class="pf-plus pf-br">+</span>
<div class="pf-content">
<span class="k">P.01 / FEATURED</span>
<h2>Any featured content, framed like a drawing.</h2>
<p>The frame assembles in reading order: left markers, horizontal lines, right markers, vertical lines. Content settles last.</p>
</div>
</div>
<script>
/* Ordered schema:
1. left "+" (tl, bl) rotate in clockwise
2. horizontal hairlines expand left → right
3. right "+" (tr, br) revealed as the lines reach them
4. vertical hairlines expand top → bottom */
function plusFrameIntro(frame){
const q = gsap.utils.selector(frame);
return gsap.timeline({defaults:{ease:"power3.out"}})
.from(q(".pf-tl, .pf-bl"), {autoAlpha:0, rotation:-120, duration:0.5, stagger:0.08})
.fromTo(q(".pf-top, .pf-bottom"), {scaleX:0}, {scaleX:1, duration:0.7, ease:"power2.inOut"}, "-=0.15")
.from(q(".pf-tr, .pf-br"), {autoAlpha:0, duration:0.3}, "-=0.12")
.fromTo(q(".pf-left, .pf-right"), {scaleY:0}, {scaleY:1, duration:0.6, ease:"power2.inOut"}, "-=0.08")
.from(q(".pf-content"), {autoAlpha:0, y:10, duration:0.5}, "-=0.25");
}
let tl = null;
const mm = gsap.matchMedia();
mm.add({
full: "(prefers-reduced-motion: no-preference)",
reduce: "(prefers-reduced-motion: reduce)"
}, (ctx) => {
if (ctx.conditions.full) tl = plusFrameIntro(document.getElementById("frame"));
/* reduce: frame stays fully rendered, no motion */
});
</script>