130Ad unit
The third-party slot — AdSense or any other network. This component is not the ad, it is everything around it: it reserves the creative's exact height at every breakpoint so nothing shifts when the fill lands, discloses that it is an ad in real DOM text, holds a skeleton while the network thinks, and collapses honestly when no fill comes back. Every format is one class that changes size by breakpoint — 320×100 on a phone becomes 728×90 on a tablet becomes 970×90 on a desktop — so a page has one slot in one place in the markup rather than three with two of them display:none, which is how a page ends up requesting three ads and rendering one.
Use it for
- Any network slot: the frame goes here, the
<ins>or iframe goes inside it - --leaderboard above the content, --halfpage or --skyscraper in a rail, --article between paragraphs
- --fluid and --article below the fold, where a responsive unit that grows on render shifts only what the reader has already passed
- The dummy creative during layout and local development, so a slot that never fills is still visible in a screenshot
Not for
- The Sponsor card. That one is a native placement we design, write and control; this is a box for someone else's artwork
- A frame with no reserved height.
min-height:0plus a late creative is the single largest source of layout shift on an ad-funded page - A slot above the reader's current line that can grow. Growing downward still moves the sentence they are reading
- Chrome around a live creative — a border on a filled display unit makes it look native, which is the one thing it must not look
- An interstitial on entry, or an anchor with no close button. Both are policy violations on every network that sells the format, and both are dark patterns before they are that
The responsive leaderboard
One class, three sizes. The phone step is not a scaled-down 728 — it is the 320×100 mobile leaderboard, a unit the networks sell separately, because a 728px creative squeezed into a phone is unreadable and a phone showing nothing is unsold inventory. Resize the window: the frame changes size, the markup does not.
markup
<div class="ns-ad ns-ad--leaderboard" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame">
<span class="ns-ad__dummy" aria-hidden="true">
<span class="ns-ad__dummy-name">Leaderboard</span>
<span class="ns-ad__dummy-size">320x100 · 728x90 · 970x90</span>
</span>
</div>
</div>
<div class="ns-ad ns-ad--billboard" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame">
<span class="ns-ad__dummy" aria-hidden="true">
<span class="ns-ad__dummy-name">Billboard</span>
<span class="ns-ad__dummy-size">300x250 · 728x90 · 970x250</span>
</span>
</div>
</div>Skeleton while it loads
data-state="loading" is the default, and the space is already held at full height before a single byte of the creative has arrived — which is the entire job. The mono size stamp is a development aid that costs nothing in production: a slot that never fills is invisible in a screenshot unless it says what it was waiting for.
markup
<div class="ns-ad ns-ad--leaderboard" data-state="loading">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame">
<span class="ns-ad__skeleton ns-skeleton" aria-hidden="true">728x90</span>
</div>
</div>
<div class="ns-ad ns-ad--rectangle" data-state="loading">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame">
<span class="ns-ad__skeleton ns-skeleton" aria-hidden="true">300x250</span>
</div>
</div>Rectangles and squares
The in-content workhorses. --rectangle (300×250) is the most-sold display size on the internet; --rectangle-lg (336×280) is the same shape given a rail that can afford it; --square steps 250×250 → 300×250 at tablet.
markup
<div class="ns-ad ns-ad--rectangle" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">Medium rectangle</span><span class="ns-ad__dummy-size">300x250</span></span></div>
</div>
<div class="ns-ad ns-ad--rectangle-lg" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">Large rectangle</span><span class="ns-ad__dummy-size">336x280</span></span></div>
</div>
<div class="ns-ad ns-ad--square" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">Square</span><span class="ns-ad__dummy-size">250x250 · 300x250</span></span></div>
</div>The tall rail units
--halfpage (300×600), --skyscraper (160×600) and --skyscraper-sm (120×600). Wrap any of them in .ns-ad-rail and the unit sticks as the reader goes down a long article — sticky rather than fixed, so it stops at the end of its own column instead of floating over the footer.
markup
<div style="display:flex;gap:var(--space-5);align-items:flex-start">
<div class="ns-ad ns-ad--halfpage" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">Half page</span><span class="ns-ad__dummy-size">300x600</span></span></div>
</div>
<div class="ns-ad ns-ad--skyscraper ns-ad-rail" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">Skyscraper</span><span class="ns-ad__dummy-size">160x600</span></span></div>
</div>
<div class="ns-ad ns-ad--skyscraper-sm" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">Narrow</span><span class="ns-ad__dummy-size">120x600</span></span></div>
</div>
</div>Phone-only banners
--banner (320×50) and --banner-lg (320×100) are hidden above 48rem, deliberately: a 320×50 banner on a desktop is not a small ad, it is a broken one. On this page they disappear when the window is wide — that is the component working.
markup
<div class="ns-ad ns-ad--banner" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-size">320x50</span></span></div>
</div>
<div class="ns-ad ns-ad--banner-lg" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-size">320x100</span></span></div>
</div>In-article
The native unit inside the prose, set at the reading measure with rules above and below — the same “this is an aside” device the article sponsor uses, so a reader who has learned one has learned both. Highest revenue and highest risk: an in-article unit that is not clearly labelled reads as a paragraph the writer wrote.
A GROUP BY on a field with high cardinality will hit the query ceiling on a real org, and the governor limit is not negotiable from Apex.
The way around it is to aggregate in the database rather than in the loop, which is the whole reason aggregate functions exist.
markup
<div class="ns-prose" style="inline-size:100%">
<p>A <code>GROUP BY</code> on a field with high cardinality will hit the query ceiling on a real org, and the governor limit is not negotiable from Apex.</p>
</div>
<div class="ns-ad ns-ad--article" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">In-article</span><span class="ns-ad__dummy-size">fluid</span></span></div>
</div>
<div class="ns-prose" style="inline-size:100%">
<p>The way around it is to aggregate in the database rather than in the loop, which is the whole reason aggregate functions exist.</p>
</div>In-feed
Between the cards in a listing, so it has to be card-shaped without being mistakable for a card. It keeps the dashed edge even when filled: in a grid of eight real courses, the one that is an ad has to be findable at a glance.
markup
<div class="ns-grid" style="grid-template-columns:repeat(auto-fill,minmax(13rem,1fr));gap:var(--gap-grid);inline-size:100%">
<div class="ns-card"><div class="ns-card__body"><span class="ns-card__kicker">// Course</span><span class="ns-card__title">Apex basics</span></div></div>
<div class="ns-ad ns-ad--feed" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">In-feed</span><span class="ns-ad__dummy-size">fluid</span></span></div>
</div>
<div class="ns-card"><div class="ns-card__body"><span class="ns-card__kicker">// Course</span><span class="ns-card__title">Flow builder</span></div></div>
</div>Multiplex
The grid of native recommendations — several ads in one block, so it is labelled once, at the top, for the whole thing.
markup
<div class="ns-ad ns-ad--multiplex" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame">
<div class="ns-ad__grid">
<span class="ns-ad__tile"><span class="ns-ad__tile-thumb"></span><span class="ns-ad__tile-title">Ship your first managed package</span><span class="ns-ad__tile-src">orgforce.dev</span></span>
<span class="ns-ad__tile"><span class="ns-ad__tile-thumb"></span><span class="ns-ad__tile-title">Static analysis for Apex, free tier</span><span class="ns-ad__tile-src">lintforce.io</span></span>
<span class="ns-ad__tile"><span class="ns-ad__tile-thumb"></span><span class="ns-ad__tile-title">Sandbox seeding in ninety seconds</span><span class="ns-ad__tile-src">seedbox.app</span></span>
</div>
</div>
</div>Parallax
A creative held still while the page scrolls over it, done with background-attachment: fixed on a clipped frame rather than a scroll listener — no JavaScript, nothing to jank. iOS Safari ignores it and falls back to a normal scrolling background, which is a graceful degradation rather than a bug to fight with a transform hack. Off entirely under reduced motion. Scroll the page to see it.
markup
<div class="ns-ad ns-ad--parallax" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame" style="background-image:linear-gradient(135deg,var(--color-brand-700),var(--color-brand-400))">
<span class="ns-ad__dummy" aria-hidden="true" style="background-color:transparent;background-image:none;border:0"><span class="ns-ad__dummy-name" style="color:var(--color-on-dark)">Parallax</span><span class="ns-ad__dummy-size" style="color:var(--color-on-dark)">fluid x 320</span></span>
</div>
</div>Anchor — the floating bottom strip
The most intrusive format still defensible, and only because of what is attached to it: a real close button at a full touch target, .ns-ad-anchored on the page shell so the last paragraph clears it, and --z-sticky rather than --z-overlay so the player bar or a save bar is never covered by an ad. Shown here in place rather than fixed, so it does not follow you around the styleguide — the page shell around it carries .ns-ad-anchored, which is what reserves the clearance.
markup
<div class="ns-ad-anchored" style="position:relative;inline-size:100%;padding-block-start:var(--space-8)">
<div class="ns-ad ns-ad--anchor" data-state="filled" style="position:static">
<button type="button" class="ns-ad__dismiss" aria-label="Close advertisement"><i class="ph ph-x" aria-hidden="true"></i></button>
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-size">320x100 · 728x90</span></span></div>
</div>
</div>Interstitial
Inside the system's real <dialog>, so focus trapping, Esc and the backdrop come from the platform. The close button is present from the first frame at full size — an interstitial whose close control is a 12px grey × after a three-second delay is a dark pattern before it is a policy violation, and it is both.
markup
<button class="ns-btn ns-btn--outline" id="ad-interstitial-open">Show interstitial</button>
<dialog class="ns-modal ns-ad-modal" id="ad-interstitial" aria-label="Advertisement">
<button class="ns-btn ns-btn--quiet ns-btn--icon ns-ad-modal__close" id="ad-interstitial-close" aria-label="Close advertisement"><i class="ph ph-x" aria-hidden="true"></i></button>
<div class="ns-ad" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">Interstitial</span><span class="ns-ad__dummy-size">336x280 · 300x600</span></span></div>
</div>
</dialog>Empty and blocked
The two states everyone forgets. empty means no fill came back — add data-collapse and the slot removes itself entirely, because the reservation existed to stop a shift while the ad was in flight and once the answer is “there is no ad” the honest thing is to give the space back. blocked says so once, quietly, and never nags: “please disable your ad blocker” is a demand, and a line about membership is an offer.
markup
<div class="ns-ad ns-ad--rectangle" data-state="empty">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__note"><span class="ns-ad__note-title">No fill</span>This slot collapses in production.</span></div>
</div>
<div class="ns-ad ns-ad--rectangle" data-state="blocked">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__note"><span class="ns-ad__note-title">Ads are off</span>Members read without them — and so do you, right now.</span></div>
</div>Fluid / responsive auto
The network sizes this one itself, which means its height is unknown until it renders — the exact thing the reservation exists to prevent. The compromise the whole industry landed on: reserve the minimum the unit can serve and let it grow downward only. Which is why a fluid unit belongs below the fold, never above the reader's current line.
markup
<div class="ns-ad ns-ad--fluid" data-state="filled" style="max-inline-size:40rem">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame"><span class="ns-ad__dummy" aria-hidden="true"><span class="ns-ad__dummy-name">Responsive</span><span class="ns-ad__dummy-size">100% x min 250</span></span></div>
</div>In place, on a real page
Every placement wired into an actual blog post: leaderboard above the masthead, skyscraper in the rail, in-article after the first section, parallax deep in the scroll, fluid at the end of the body, multiplex and in-feed at the foot, a dismissible anchor and an interstitial behind a button. The skeletons are live — the slots start in loading and flip to filled after a beat, so you can watch the reservation hold. It carries every format at once and no real page should; the template says so at the top and names the three worth keeping. The responsive page is the same post in three real iframes, which is the only honest way to show a size ladder — media queries answer to the viewport, so a narrow div would keep serving the desktop unit.
markup
<p><a class="ns-btn ns-btn--outline ns-btn--sm" href="./demo-blog-ads.html" target="_blank" rel="noopener">Blog post with ads <i class="ph ph-arrow-up-right" aria-hidden="true"></i></a>
<a class="ns-btn ns-btn--outline ns-btn--sm" href="./demo-blog-ads-responsive.html" target="_blank" rel="noopener">The ladder at three widths <i class="ph ph-arrow-up-right" aria-hidden="true"></i></a></p>The dummy creative
Deliberately hatched and stamped with its own dimensions, so nobody screenshots a page for a client and ships a placeholder believing it was a live unit. Use it for layout work and local development where no network is wired up.
markup
<div class="ns-ad ns-ad--rectangle" data-state="filled">
<span class="ns-ad__label">Advertisement</span>
<div class="ns-ad__frame">
<span class="ns-ad__dummy" aria-hidden="true">
<span class="ns-ad__dummy-name">Your ad here</span>
<span class="ns-ad__dummy-size">300x250</span>
</span>
</div>
</div>Accessibility contract
- The disclosure is the visible label, in the DOM, as real text — never an aria-label and never a background image. A screen reader user gets the same warning from the same node, so the two cannot drift
- The interstitial is a real <dialog>, so focus trapping, Esc-to-close and inertness of the page behind come from the platform rather than from a script that gets it 80% right
- The anchor's close button is a full --target-min touch target, present from the first frame
- --parallax is motion tied to scroll, so it is switched off under prefers-reduced-motion rather than slowed down
- Ads are removed in print — a reserved empty frame prints as a blank box in the middle of the article