20 CSS Blog Layouts19 / 20

Pure CSSMIT licensed

Inline Newsletter Signup Callout

The mid-article subscribe block that converts without wrecking the read: it breaks out wider than the measure using named grid lines (never negative margins), validates with :user-invalid so no error appears until you have actually finished typing, and shows a real success state driven by :valid — all without JavaScript.

Published

Live Demo
Try it

The code

<article class="bl-19">
  <div class="bl-19__prose">
    <p class="bl-19__rubric">Newsletters</p>
    <h1>Why the mid-article signup outperforms every sidebar you have ever built</h1>
    <p class="bl-19__lede">Sidebars are furniture. Readers learn to skip them within two page views. A callout placed where attention is already highest behaves completely differently — and the difference is not subtle.</p>

    <p>The pattern is old and the reasoning is dull: a reader who has finished four paragraphs has demonstrated interest, and a request made at that moment is answering a question they have already asked themselves. A sidebar makes the same offer to someone who has demonstrated nothing.</p>
    <p>What matters is where exactly. Too early and it reads as a paywall tease; too late and the reader has already left or has already decided. In our own logs the best position was consistently after the first substantive section — roughly 35% of the way down — and the second best was directly before the conclusion.</p>

    <aside class="bl-19__cta" aria-labelledby="bl-19-cta-h">
      <div class="bl-19__ctaText">
        <p class="bl-19__ctaTag">The Thursday letter</p>
        <h2 id="bl-19-cta-h">One idea about interface craft, every week</h2>
        <p>Written for people who ship. No sponsors, no roundups, no “5 tools you missed”. Unsubscribe in one click, and the archive stays open.</p>
        <ul class="bl-19__proof">
          <li><strong>11,400</strong> readers</li>
          <li><strong>64%</strong> open rate</li>
          <li><strong>142</strong> issues</li>
        </ul>
      </div>
      <div class="bl-19__form">
        <div class="bl-19__field">
          <label for="bl-19-email">Email address</label>
          <input id="bl-19-email" type="email" inputmode="email" autocomplete="email" placeholder="you@studio.com" required>
          <p class="bl-19__err" role="alert">That doesn't look like an email address yet.</p>
          <p class="bl-19__ok">Looks good — hit subscribe.</p>
        </div>
        <button class="bl-19__submit" type="button">Subscribe free</button>
        <p class="bl-19__fine">We store your email and nothing else. <a href="#">Read the two-line privacy note</a>.</p>
      </div>
    </aside>

    <p>The implementation detail people get wrong is the interruption cost. A callout that animates in, sticks, or expands on scroll converts worse than a static block, because the reader's first reaction is to find the close button rather than the offer. Stillness is a feature.</p>
    <p>The second detail is validation. A required email field that turns red before you have typed anything is the most common form of self-inflicted friction on the web, and it has had a one-word fix since 2021: <code>:user-invalid</code> instead of <code>:invalid</code>.</p>
    <p>Everything else is copy. Say what the letter is, how often it arrives, and what happens to the address. Three sentences beat three paragraphs, and a specific number beats “join thousands of readers”.</p>
    <p class="bl-19__coda">This block is the pattern it describes — it breaks out of the measure with grid lines, not negative margins, and validates on blur.</p>
  </div>
</article>
@import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,600;1,6..72,400&family=Syne:wght@600;700;800&display=swap');

.bl-19 {
  --bg: #fcfbf9;
  --surface: #ffffff;
  --text: #151412;
  --muted: #6b665e;
  --line: #e6e2da;
  --acc: #4c1d95;
  --good: #15803d;
  --bad: #b91c1c;
  --flow: 1.5em;
  --serif: "Newsreader",ui-serif,Georgia,serif;
  --display: "Syne",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
}

@supports (color: oklch(50% 0 0)) {
  .bl-19 {
    --bg: oklch(98.5% .005 90);
    --surface: oklch(100% 0 0);
    --text: oklch(17% .008 70);
    --muted: oklch(50% .012 72);
    --line: oklch(90% .01 85);
    --acc: oklch(38% .18 295);
    --good: oklch(50% .14 148);
    --bad: oklch(50% .19 27);
  }
}

@media (prefers-color-scheme: dark) {
  .bl-19:not([data-theme="light"]) {
    --bg: #100f0e;
    --surface: #191816;
    --text: #f2f0ec;
    --muted: #a09a91;
    --line: #2a2724;
    --acc: #c4b5fd;
    --good: #4ade80;
    --bad: #f87171;
  }
}

.bl-19[data-theme="dark"] {
  --bg: #100f0e;
  --surface: #191816;
  --text: #f2f0ec;
  --muted: #a09a91;
  --line: #2a2724;
  --acc: #c4b5fd;
  --good: #4ade80;
  --bad: #f87171;
}

.bl-19 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--bg);
  color: var(--text);
  font-family: var(--serif);
  -webkit-font-smoothing: antialiased;
}

.bl-19,
.bl-19 *,
.bl-19 *::before,
.bl-19 *::after {
  box-sizing: border-box;
}

.bl-19 a {
  color: inherit;
}

.bl-19 :focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
  border-radius: 3px;
}

.bl-19__prose {
  display: grid;
  grid-template-columns: [wide-start] minmax(1.25rem,1fr) [text-start] min(66ch,100% - 2.5rem) [text-end] minmax(1.25rem,1fr) [wide-end];
  padding-block: clamp(2.5rem,7vw,5rem) clamp(4rem,10vw,7rem);
  font-size: clamp(1.125rem,1.4vw,1.25rem);
  line-height: 1.74;
}

.bl-19__prose > * {
  grid-column: text;
  margin: 0;
}

.bl-19__prose > * + * {
  margin-block-start: var(--flow);
}

.bl-19__rubric {
  font-family: var(--display);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--acc);
}

.bl-19__prose h1 {
  --flow: .5em;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(1.875rem,5vw,3.125rem);
  line-height: 1.04;
  letter-spacing: -.035em;
  text-wrap: balance;
}

.bl-19__lede {
  font-size: 1.2em;
  line-height: 1.56;
  color: var(--muted);
  text-wrap: pretty;
}

.bl-19__prose p {
  text-wrap: pretty;
}

.bl-19__prose code {
  font-family: ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size: .85em;
  padding: .1em .3em;
  border-radius: .25rem;
  background: color-mix(in oklab,var(--acc) 12%,transparent);
}

.bl-19__cta {
  grid-column: wide;
  --flow: 2.6em;
  display: grid;
  grid-template-columns: minmax(0,1.15fr) minmax(0,1fr);
  gap: clamp(1.25rem,3vw,2.5rem);
  align-items: start;
  max-width: 66rem;
  margin-inline: auto;
  width: 100%;
  padding: clamp(1.5rem,3.5vw,2.5rem);
  border-radius: 1.25rem;
  background: linear-gradient(135deg,color-mix(in oklab,var(--acc) 10%,var(--surface)),var(--surface) 65%);
  border: 1px solid color-mix(in oklab,var(--acc) 25%,var(--line));
}

.bl-19__ctaText {
  display: grid;
  gap: .7rem;
}

.bl-19__ctaTag {
  margin: 0;
  font-family: var(--display);
  font-size: .625rem;
  font-weight: 700;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--acc);
}

.bl-19__cta h2 {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(1.375rem,2.8vw,1.875rem);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -.03em;
  text-wrap: balance;
}

.bl-19__ctaText p {
  margin: 0;
  font-size: 1rem;
  line-height: 1.62;
  color: var(--muted);
  text-wrap: pretty;
}

.bl-19__proof {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  margin: .35rem 0 0;
  padding: 0;
  list-style: none;
}

.bl-19__proof li {
  font-family: var(--display);
  font-size: .75rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

.bl-19__proof strong {
  display: block;
  font-size: 1.375rem;
  letter-spacing: -.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.bl-19__form {
  display: grid;
  gap: .6rem;
  align-self: center;
}

.bl-19__field {
  display: grid;
  gap: .35rem;
}

.bl-19__field label {
  font-family: var(--display);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
}

.bl-19__field input {
  min-height: 3.25rem;
  padding: 0 1rem;
  border-radius: .625rem;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--text);
  font-family: var(--display);
  font-size: 1rem;
  transition: border-color .18s ease, box-shadow .18s ease;
}

.bl-19__field input::placeholder {
  color: color-mix(in oklab,var(--muted) 70%,transparent);
}

.bl-19__field input:focus-visible {
  border-color: var(--acc);
  box-shadow: 0 0 0 3px color-mix(in oklab,var(--acc) 18%,transparent);
  outline: 0;
}

.bl-19__field input:user-invalid {
  border-color: var(--bad);
}

.bl-19__field input:user-valid {
  border-color: var(--good);
}

.bl-19__err,
.bl-19__ok {
  margin: 0;
  display: none;
  font-family: var(--display);
  font-size: .75rem;
  line-height: 1.4;
}

.bl-19__err {
  color: var(--bad);
}

.bl-19__ok {
  color: var(--good);
}

.bl-19__field:has(input:user-invalid) .bl-19__err {
  display: block;
}

.bl-19__field:has(input:user-valid) .bl-19__ok {
  display: block;
}

.bl-19__submit {
  min-height: 3.25rem;
  padding: 0 1.5rem;
  border: 0;
  border-radius: .625rem;
  cursor: pointer;
  background: var(--acc);
  color: #fff;
  font-family: var(--display);
  font-size: .9375rem;
  font-weight: 700;
  letter-spacing: .01em;
  transition: filter .18s ease, translate .18s cubic-bezier(.16,1,.3,1);
}

.bl-19__submit:hover {
  filter: brightness(1.12);
  translate: 0 -1px;
}

.bl-19__fine {
  margin: 0;
  font-family: var(--display);
  font-size: .6875rem;
  line-height: 1.5;
  color: var(--muted);
}

.bl-19__fine a {
  color: var(--acc);
  text-decoration: underline;
  text-underline-offset: .2em;
}

.bl-19__coda {
  --flow: 2em;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  font-family: var(--display);
  font-size: .875rem !important;
  line-height: 1.6 !important;
  color: var(--muted);
}

@media (max-width: 52rem) {
  .bl-19__cta {
    grid-template-columns: minmax(0,1fr);
  }
}

@media (prefers-reduced-motion: reduce) {
  .bl-19 * {
    transition: none !important;
    animation: none !important;
  }
}

@media (forced-colors: active) {
  .bl-19__cta {
    border: 1px solid CanvasText;
  }

  .bl-19__submit {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Blog Layout from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Inline Newsletter Signup Callout
Source: https://codefronts.com/layouts/css-blog-layouts/inline-newsletter-signup-callout/

The mid-article subscribe block that converts without wrecking the read: it breaks out wider than the measure using named grid lines (never negative margins), validates with :user-invalid so no error appears until you have actually finished typing, and shows a real success state driven by :valid — all without JavaScript.
## HTML
```html
<article class="bl-19">
  <div class="bl-19__prose">
    <p class="bl-19__rubric">Newsletters</p>
    <h1>Why the mid-article signup outperforms every sidebar you have ever built</h1>
    <p class="bl-19__lede">Sidebars are furniture. Readers learn to skip them within two page views. A callout placed where attention is already highest behaves completely differently — and the difference is not subtle.</p>

    <p>The pattern is old and the reasoning is dull: a reader who has finished four paragraphs has demonstrated interest, and a request made at that moment is answering a question they have already asked themselves. A sidebar makes the same offer to someone who has demonstrated nothing.</p>
    <p>What matters is where exactly. Too early and it reads as a paywall tease; too late and the reader has already left or has already decided. In our own logs the best position was consistently after the first substantive section — roughly 35% of the way down — and the second best was directly before the conclusion.</p>

    <aside class="bl-19__cta" aria-labelledby="bl-19-cta-h">
      <div class="bl-19__ctaText">
        <p class="bl-19__ctaTag">The Thursday letter</p>
        <h2 id="bl-19-cta-h">One idea about interface craft, every week</h2>
        <p>Written for people who ship. No sponsors, no roundups, no “5 tools you missed”. Unsubscribe in one click, and the archive stays open.</p>
        <ul class="bl-19__proof">
          <li><strong>11,400</strong> readers</li>
          <li><strong>64%</strong> open rate</li>
          <li><strong>142</strong> issues</li>
        </ul>
      </div>
      <div class="bl-19__form">
        <div class="bl-19__field">
          <label for="bl-19-email">Email address</label>
          <input id="bl-19-email" type="email" inputmode="email" autocomplete="email" placeholder="you@studio.com" required>
          <p class="bl-19__err" role="alert">That doesn't look like an email address yet.</p>
          <p class="bl-19__ok">Looks good — hit subscribe.</p>
        </div>
        <button class="bl-19__submit" type="button">Subscribe free</button>
        <p class="bl-19__fine">We store your email and nothing else. <a href="#">Read the two-line privacy note</a>.</p>
      </div>
    </aside>

    <p>The implementation detail people get wrong is the interruption cost. A callout that animates in, sticks, or expands on scroll converts worse than a static block, because the reader's first reaction is to find the close button rather than the offer. Stillness is a feature.</p>
    <p>The second detail is validation. A required email field that turns red before you have typed anything is the most common form of self-inflicted friction on the web, and it has had a one-word fix since 2021: <code>:user-invalid</code> instead of <code>:invalid</code>.</p>
    <p>Everything else is copy. Say what the letter is, how often it arrives, and what happens to the address. Three sentences beat three paragraphs, and a specific number beats “join thousands of readers”.</p>
    <p class="bl-19__coda">This block is the pattern it describes — it breaks out of the measure with grid lines, not negative margins, and validates on blur.</p>
  </div>
</article>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,600;1,6..72,400&family=Syne:wght@600;700;800&display=swap');

.bl-19 {
  --bg: #fcfbf9;
  --surface: #ffffff;
  --text: #151412;
  --muted: #6b665e;
  --line: #e6e2da;
  --acc: #4c1d95;
  --good: #15803d;
  --bad: #b91c1c;
  --flow: 1.5em;
  --serif: "Newsreader",ui-serif,Georgia,serif;
  --display: "Syne",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
}

@supports (color: oklch(50% 0 0)) {
  .bl-19 {
    --bg: oklch(98.5% .005 90);
    --surface: oklch(100% 0 0);
    --text: oklch(17% .008 70);
    --muted: oklch(50% .012 72);
    --line: oklch(90% .01 85);
    --acc: oklch(38% .18 295);
    --good: oklch(50% .14 148);
    --bad: oklch(50% .19 27);
  }
}

@media (prefers-color-scheme: dark) {
  .bl-19:not([data-theme="light"]) {
    --bg: #100f0e;
    --surface: #191816;
    --text: #f2f0ec;
    --muted: #a09a91;
    --line: #2a2724;
    --acc: #c4b5fd;
    --good: #4ade80;
    --bad: #f87171;
  }
}

.bl-19[data-theme="dark"] {
  --bg: #100f0e;
  --surface: #191816;
  --text: #f2f0ec;
  --muted: #a09a91;
  --line: #2a2724;
  --acc: #c4b5fd;
  --good: #4ade80;
  --bad: #f87171;
}

.bl-19 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--bg);
  color: var(--text);
  font-family: var(--serif);
  -webkit-font-smoothing: antialiased;
}

.bl-19,
.bl-19 *,
.bl-19 *::before,
.bl-19 *::after {
  box-sizing: border-box;
}

.bl-19 a {
  color: inherit;
}

.bl-19 :focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 3px;
  border-radius: 3px;
}

.bl-19__prose {
  display: grid;
  grid-template-columns: [wide-start] minmax(1.25rem,1fr) [text-start] min(66ch,100% - 2.5rem) [text-end] minmax(1.25rem,1fr) [wide-end];
  padding-block: clamp(2.5rem,7vw,5rem) clamp(4rem,10vw,7rem);
  font-size: clamp(1.125rem,1.4vw,1.25rem);
  line-height: 1.74;
}

.bl-19__prose > * {
  grid-column: text;
  margin: 0;
}

.bl-19__prose > * + * {
  margin-block-start: var(--flow);
}

.bl-19__rubric {
  font-family: var(--display);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--acc);
}

.bl-19__prose h1 {
  --flow: .5em;
  font-family: var(--display);
  font-weight: 800;
  font-size: clamp(1.875rem,5vw,3.125rem);
  line-height: 1.04;
  letter-spacing: -.035em;
  text-wrap: balance;
}

.bl-19__lede {
  font-size: 1.2em;
  line-height: 1.56;
  color: var(--muted);
  text-wrap: pretty;
}

.bl-19__prose p {
  text-wrap: pretty;
}

.bl-19__prose code {
  font-family: ui-monospace,SFMono-Regular,Menlo,monospace;
  font-size: .85em;
  padding: .1em .3em;
  border-radius: .25rem;
  background: color-mix(in oklab,var(--acc) 12%,transparent);
}

.bl-19__cta {
  grid-column: wide;
  --flow: 2.6em;
  display: grid;
  grid-template-columns: minmax(0,1.15fr) minmax(0,1fr);
  gap: clamp(1.25rem,3vw,2.5rem);
  align-items: start;
  max-width: 66rem;
  margin-inline: auto;
  width: 100%;
  padding: clamp(1.5rem,3.5vw,2.5rem);
  border-radius: 1.25rem;
  background: linear-gradient(135deg,color-mix(in oklab,var(--acc) 10%,var(--surface)),var(--surface) 65%);
  border: 1px solid color-mix(in oklab,var(--acc) 25%,var(--line));
}

.bl-19__ctaText {
  display: grid;
  gap: .7rem;
}

.bl-19__ctaTag {
  margin: 0;
  font-family: var(--display);
  font-size: .625rem;
  font-weight: 700;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--acc);
}

.bl-19__cta h2 {
  margin: 0;
  font-family: var(--display);
  font-size: clamp(1.375rem,2.8vw,1.875rem);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -.03em;
  text-wrap: balance;
}

.bl-19__ctaText p {
  margin: 0;
  font-size: 1rem;
  line-height: 1.62;
  color: var(--muted);
  text-wrap: pretty;
}

.bl-19__proof {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  margin: .35rem 0 0;
  padding: 0;
  list-style: none;
}

.bl-19__proof li {
  font-family: var(--display);
  font-size: .75rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

.bl-19__proof strong {
  display: block;
  font-size: 1.375rem;
  letter-spacing: -.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

.bl-19__form {
  display: grid;
  gap: .6rem;
  align-self: center;
}

.bl-19__field {
  display: grid;
  gap: .35rem;
}

.bl-19__field label {
  font-family: var(--display);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
}

.bl-19__field input {
  min-height: 3.25rem;
  padding: 0 1rem;
  border-radius: .625rem;
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--text);
  font-family: var(--display);
  font-size: 1rem;
  transition: border-color .18s ease, box-shadow .18s ease;
}

.bl-19__field input::placeholder {
  color: color-mix(in oklab,var(--muted) 70%,transparent);
}

.bl-19__field input:focus-visible {
  border-color: var(--acc);
  box-shadow: 0 0 0 3px color-mix(in oklab,var(--acc) 18%,transparent);
  outline: 0;
}

.bl-19__field input:user-invalid {
  border-color: var(--bad);
}

.bl-19__field input:user-valid {
  border-color: var(--good);
}

.bl-19__err,
.bl-19__ok {
  margin: 0;
  display: none;
  font-family: var(--display);
  font-size: .75rem;
  line-height: 1.4;
}

.bl-19__err {
  color: var(--bad);
}

.bl-19__ok {
  color: var(--good);
}

.bl-19__field:has(input:user-invalid) .bl-19__err {
  display: block;
}

.bl-19__field:has(input:user-valid) .bl-19__ok {
  display: block;
}

.bl-19__submit {
  min-height: 3.25rem;
  padding: 0 1.5rem;
  border: 0;
  border-radius: .625rem;
  cursor: pointer;
  background: var(--acc);
  color: #fff;
  font-family: var(--display);
  font-size: .9375rem;
  font-weight: 700;
  letter-spacing: .01em;
  transition: filter .18s ease, translate .18s cubic-bezier(.16,1,.3,1);
}

.bl-19__submit:hover {
  filter: brightness(1.12);
  translate: 0 -1px;
}

.bl-19__fine {
  margin: 0;
  font-family: var(--display);
  font-size: .6875rem;
  line-height: 1.5;
  color: var(--muted);
}

.bl-19__fine a {
  color: var(--acc);
  text-decoration: underline;
  text-underline-offset: .2em;
}

.bl-19__coda {
  --flow: 2em;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  font-family: var(--display);
  font-size: .875rem !important;
  line-height: 1.6 !important;
  color: var(--muted);
}

@media (max-width: 52rem) {
  .bl-19__cta {
    grid-template-columns: minmax(0,1fr);
  }
}

@media (prefers-reduced-motion: reduce) {
  .bl-19 * {
    transition: none !important;
    animation: none !important;
  }
}

@media (forced-colors: active) {
  .bl-19__cta {
    border: 1px solid CanvasText;
  }

  .bl-19__submit {
    forced-color-adjust: none;
    background: Highlight;
    color: HighlightText;
  }
}
```

How this works

Breakout is a grid problem. The prose declares two tracks; the callout opts into the wider one:

.bl-19__prose{ grid-template-columns:[wide-start] 1fr [text-start] min(66ch,100%) [text-end] 1fr [wide-end]; }
.bl-19__prose > *{ grid-column:text; }
.bl-19__cta{ grid-column:wide; }

This is strictly better than margin-inline:calc(50% - 50vw), which overflows horizontally whenever a scrollbar is present because 100vw includes it.

Validation uses the modern pseudo-classes, which fire on blur rather than on every keystroke:

.bl-19__field input:user-invalid{ border-color:var(--bad); }
.bl-19__field input:user-valid{ border-color:var(--good); }
.bl-19__field:has(input:user-invalid) .bl-19__err{ display:block; }

:invalid would mark an empty required field as wrong before the reader has typed a character — the classic hostile form. :user-invalid exists precisely to fix that, and it is now supported everywhere current.

The whole block is inert to the article's flow: it uses the same --flow spacing token as paragraphs, so it never introduces a rhythm break.

Make it yours

  • Breakout width is the 1fr gutter; cap it with minmax(1rem,6rem) to keep the callout close to the measure or let it run to the page edge.
  • Swap the layout at @container width instead of viewport width if the callout appears in narrow slots too.
  • Social proof line, avatar stack, or a sample-issue link — pick one, not all three. The measured winner is usually a specific subscriber count.
  • Add a honeypot field (visually hidden, tabindex="-1", autocomplete="off") for spam protection with no CAPTCHA.
  • For a paid tier, duplicate the block with data-variant="paid" and change the accent token only.

Gotchas — read before shipping

  • Never use :invalid for live styling — it matches empty required fields immediately. :user-invalid waits for interaction.
  • calc(50% - 50vw) breakouts overflow when a scrollbar exists. Use grid tracks, or 100dvw with scrollbar-gutter:stable.
  • An email input must be type="email" with inputmode="email" and autocomplete="email" — the three attributes that make mobile entry bearable.
  • Placeholders are not labels. A visible label (or a visually-hidden one plus a persistent hint) is required for screen readers and for anyone who forgets what the field wanted.
  • A CTA interrupting the prose must not be sticky, animated, or auto-expanding: those are the patterns readers report as the most hostile in reading studies.
  • Don't put the subscribe block between a heading and its first paragraph — it visually orphans the heading.

Browser support

ChromeSafariFirefoxEdge
119+16.5+88+119+

:user-invalid / :user-valid need Chrome 119, Safari 16.5, Firefox 88; where missing, the field simply has no live validation styling and the form still submits. Grid named lines, :has() and clamp() are supported in every current browser.

Techniques used in this demo

Search CodeFronts

Loading…