Pure CSSMIT licensed

Textured Section Divider with SVG Grain

Two coloured sections meeting at a hard line is the most obvious seam in web design. This divider replaces it with a torn, grained transition: a mask driven by feTurbulence so the edge dissolves irregularly instead of stepping, and a grain band that carries texture across the join. Three divider variants — torn, fade, and ruled — in one page.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: a 10,000+ component Figma UI kit · from $129 one-time

Untitled UI · Affiliate link

Get Untitled UI Pro

The code

<section class="grn-13" aria-labelledby="grn-13-title">
  <svg class="grn-13__defs" width="0" height="0" aria-hidden="true" focusable="false">
    <filter id="grn-13-tear" x="-20%" y="-40%" width="140%" height="180%" color-interpolation-filters="sRGB">
      <feTurbulence type="fractalNoise" baseFrequency="0.012 0.34" numOctaves="3" seed="23" result="grn13noise"/>
      <feDisplacementMap in="SourceGraphic" in2="grn13noise" scale="18" xChannelSelector="R" yChannelSelector="G"/>
    </filter>
  </svg>

  <div class="grn-13__theme">
    <input class="grn-13__theme-input" type="checkbox" id="grn-13-theme">
    <label class="grn-13__theme-label" for="grn-13-theme">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="grn-13__sr">Toggle dark theme</span>
    </label>
  </div>

  <section class="grn-13__band grn-13__band--top" aria-labelledby="grn-13-title">
    <div class="grn-13__inner">
      <p class="grn-13__kicker">Field guide · issue 12</p>
      <h1 class="grn-13__h1" id="grn-13-title">Sections should meet like materials, not like divs</h1>
      <p class="grn-13__lede">Below this paragraph the edge is torn by an SVG displacement map, and the grain band runs straight across the join. That continuity is what makes it read as one surface.</p>
    </div>
    <span class="grn-13__tear" aria-hidden="true"></span>
    <span class="grn-13__band-grain" aria-hidden="true"></span>
  </section>

  <section class="grn-13__band grn-13__band--mid" aria-labelledby="grn-13-h-fade">
    <span class="grn-13__band-grain" aria-hidden="true"></span>
    <div class="grn-13__inner">
      <h2 class="grn-13__h2" id="grn-13-h-fade">The grained fade</h2>
      <p class="grn-13__body">The workhorse. A <code>linear-gradient</code> mask dissolves the section boundary over 8rem while the grain layer continues at identical weight on both sides. Use it in long documents where you want sections to breathe without adding visual events. It costs one mask and no extra DOM.</p>
      <ul class="grn-13__facts">
        <li><b>Mask</b><span>linear-gradient, 8rem</span></li>
        <li><b>Grain</b><span>continuous, 0.05</span></li>
        <li><b>DOM added</b><span>none</span></li>
      </ul>
    </div>
    <span class="grn-13__fade" aria-hidden="true"></span>
  </section>

  <section class="grn-13__band grn-13__band--bottom" aria-labelledby="grn-13-h-rule">
    <span class="grn-13__band-grain" aria-hidden="true"></span>
    <div class="grn-13__inner">
      <h2 class="grn-13__h2" id="grn-13-h-rule">The ruled divider that does not look like a border</h2>
      <p class="grn-13__body">A hairline with a grain wash spilling past it in both directions. Utilitarian, appropriate in dashboards and documentation, and the only one of the three that survives being used a dozen times on one page.</p>
      <p class="grn-13__foot-note">Pick one divider per page. This demo shows all three because it is a specimen sheet — shipping all three would be a design failure, not a feature.</p>
    </div>
  </section>
</section>
.grn-13 {
  --grn-13-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170'%3E%3Cfilter id='grn13n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='170' height='170' filter='url(%23grn13n)'/%3E%3C/svg%3E");
  --grn-13-top: #e9ecdf;
  --grn-13-top: oklch(93% 0.02 120);
  --grn-13-mid: #47543a;
  --grn-13-mid: oklch(42% 0.05 130);
  --grn-13-bottom: #f4f5ee;
  --grn-13-bottom: oklch(96% 0.01 120);
  --grn-13-ink: #1f2418;
  --grn-13-ink-on-mid: #eef2e4;
  --grn-13-mut: #616a55;
  --grn-13-acc: #7f9b3f;
  --grn-13-acc: oklch(62% 0.13 128);
  --grn-13-grain: 0.05;
  --grn-13-tear-h: clamp(3rem, 8vw, 6rem);
  --grn-13-font-display: "Space Grotesk", "Inter Tight", ui-sans-serif, system-ui, sans-serif;
  --grn-13-font-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", Menlo, monospace;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--grn-13-bottom);
  color: var(--grn-13-ink);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  line-height: 1.6;
}

.grn-13 *,
.grn-13 *::before,
.grn-13 *::after {
  box-sizing: border-box;
}

.grn-13 :focus-visible {
  outline: 2px solid var(--grn-13-acc);
  outline-offset: 3px;
}

.grn-13 code {
  font-family: var(--grn-13-font-mono);
  font-size: 0.9em;
}

.grn-13__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.grn-13__defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.grn-13:has(.grn-13__theme-input:checked) {
  --grn-13-top: #171a13;
  --grn-13-top: oklch(20% 0.02 125);
  --grn-13-mid: #2b3324;
  --grn-13-mid: oklch(28% 0.04 130);
  --grn-13-bottom: #101208;
  --grn-13-bottom: oklch(16% 0.02 118);
  --grn-13-ink: #e8eddd;
  --grn-13-ink-on-mid: #e8eddd;
  --grn-13-mut: #9aa68c;
  --grn-13-acc: #b6d16a;
  --grn-13-acc: oklch(83% 0.15 122);
  --grn-13-grain: 0.075;
}

@media (prefers-color-scheme: dark) {
  .grn-13 {
    --grn-13-top: #171a13;
    --grn-13-top: oklch(20% 0.02 125);
    --grn-13-mid: #2b3324;
    --grn-13-mid: oklch(28% 0.04 130);
    --grn-13-bottom: #101208;
    --grn-13-bottom: oklch(16% 0.02 118);
    --grn-13-ink: #e8eddd;
    --grn-13-ink-on-mid: #e8eddd;
    --grn-13-mut: #9aa68c;
    --grn-13-acc: #b6d16a;
    --grn-13-acc: oklch(83% 0.15 122);
    --grn-13-grain: 0.075;
  }

  .grn-13:has(.grn-13__theme-input:checked) {
    --grn-13-top: #e9ecdf;
    --grn-13-top: oklch(93% 0.02 120);
    --grn-13-mid: #47543a;
    --grn-13-mid: oklch(42% 0.05 130);
    --grn-13-bottom: #f4f5ee;
    --grn-13-bottom: oklch(96% 0.01 120);
    --grn-13-ink: #1f2418;
    --grn-13-ink-on-mid: #eef2e4;
    --grn-13-mut: #616a55;
    --grn-13-acc: #7f9b3f;
    --grn-13-acc: oklch(62% 0.13 128);
    --grn-13-grain: 0.05;
  }
}

@media (prefers-color-scheme: light) {
  .grn-13:has(.grn-13__theme-input:checked) {
    --grn-13-top: #171a13;
    --grn-13-top: oklch(20% 0.02 125);
    --grn-13-mid: #2b3324;
    --grn-13-mid: oklch(28% 0.04 130);
    --grn-13-bottom: #101208;
    --grn-13-bottom: oklch(16% 0.02 118);
    --grn-13-ink: #e8eddd;
    --grn-13-ink-on-mid: #e8eddd;
    --grn-13-mut: #9aa68c;
    --grn-13-acc: #b6d16a;
    --grn-13-acc: oklch(83% 0.15 122);
    --grn-13-grain: 0.075;
  }
}

.grn-13__theme {
  position: absolute;
  inset-block-start: clamp(1rem, 3vw, 1.75rem);
  inset-inline-end: clamp(0.85rem, 3vw, 1.75rem);
  z-index: 60;
}

.grn-13__theme-input {
  position: absolute;
  inset: 0;
  opacity: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  cursor: pointer;
}

.grn-13__theme-label {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid color-mix(in oklab, var(--grn-13-ink) 22%, transparent);
  border-radius: 999px;
  background: var(--grn-13-bottom);
  color: var(--grn-13-ink);
  cursor: pointer;
}

.grn-13__theme-label svg {
  inline-size: 20px;
  block-size: 20px;
}

.grn-13__band {
  position: relative;
  isolation: isolate;
}

.grn-13__band--top {
  background: var(--grn-13-top);
  padding-block: clamp(3.5rem, 9vw, 6rem) calc(var(--grn-13-tear-h) + clamp(2rem, 5vw, 3.5rem));
}

.grn-13__band--mid {
  background: var(--grn-13-mid);
  color: var(--grn-13-ink-on-mid);
  padding-block: clamp(3rem, 8vw, 5rem) clamp(5rem, 12vw, 8rem);
  margin-block-start: calc(var(--grn-13-tear-h) * -1);
}

.grn-13__band--bottom {
  background: var(--grn-13-bottom);
  padding-block: clamp(3rem, 8vw, 5rem) clamp(3.5rem, 9vw, 6rem);
}

.grn-13__band--bottom::before {
  content: "";
  display: block;
  block-size: 1px;
  inline-size: min(64rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
  margin: 0 auto clamp(2rem, 5vw, 3rem);
  background: color-mix(in oklab, var(--grn-13-ink) 20%, transparent);
}

.grn-13__band-grain {
  position: absolute;
  inset: calc(var(--grn-13-tear-h) * -1) 0;
  z-index: 1;
  pointer-events: none;
  background-image: var(--grn-13-noise);
  opacity: var(--grn-13-grain);
  mix-blend-mode: overlay;
}

.grn-13__tear {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 2;
  block-size: var(--grn-13-tear-h);
  background: var(--grn-13-top);
  filter: url(#grn-13-tear);
  pointer-events: none;
}

.grn-13__fade {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 2;
  block-size: clamp(5rem, 12vw, 8rem);
  background: var(--grn-13-bottom);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 82%);
  mask-image: linear-gradient(to bottom, transparent, #000 82%);
}

.grn-13__inner {
  position: relative;
  z-index: 3;
  inline-size: min(64rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
  margin-inline: auto;
  display: grid;
  gap: 0.75rem;
  max-inline-size: 46rem;
}

.grn-13__kicker {
  margin: 0;
  font: 500 0.72rem/1 var(--grn-13-font-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--grn-13-acc);
}

.grn-13__h1 {
  margin: 0;
  font: 600 clamp(2rem, 5.6vw, 3.5rem)/1.03 var(--grn-13-font-display);
  letter-spacing: -0.035em;
  text-wrap: balance;
}

.grn-13__h2 {
  margin: 0;
  font: 600 clamp(1.4rem, 3.6vw, 2.1rem)/1.1 var(--grn-13-font-display);
  letter-spacing: -0.03em;
  text-wrap: balance;
}

.grn-13__lede {
  margin: 0;
  max-inline-size: 40rem;
  color: var(--grn-13-mut);
  font-size: clamp(0.97rem, 2vw, 1.1rem);
  text-wrap: pretty;
}

.grn-13__body {
  margin: 0;
  max-inline-size: 42rem;
  font-size: clamp(0.95rem, 1.9vw, 1.05rem);
  text-wrap: pretty;
  color: color-mix(in oklab, currentColor 82%, transparent);
}

.grn-13__facts {
  list-style: none;
  margin: 0.4rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.8rem;
}

.grn-13__facts li {
  display: grid;
  gap: 0.1rem;
}

.grn-13__facts b {
  font: 600 0.8rem/1.2 var(--grn-13-font-display);
}

.grn-13__facts span {
  font: 400 0.78rem/1.2 var(--grn-13-font-mono);
  opacity: 0.75;
}

.grn-13__foot-note {
  margin: 0.5rem 0 0;
  padding-block-start: 0.8rem;
  border-block-start: 1px solid color-mix(in oklab, var(--grn-13-ink) 16%, transparent);
  color: var(--grn-13-mut);
  font-size: 0.85rem;
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .grn-13 * {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
  }
}
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 Grain and Noise Texture 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: Textured Section Divider with SVG Grain
Source: https://codefronts.com/design-styles/css-grain-texture/textured-section-divider-with-svg-grain/

Two coloured sections meeting at a hard line is the most obvious seam in web design. This divider replaces it with a torn, grained transition: a mask driven by feTurbulence so the edge dissolves irregularly instead of stepping, and a grain band that carries texture across the join. Three divider variants — torn, fade, and ruled — in one page.
## HTML
```html
<section class="grn-13" aria-labelledby="grn-13-title">
  <svg class="grn-13__defs" width="0" height="0" aria-hidden="true" focusable="false">
    <filter id="grn-13-tear" x="-20%" y="-40%" width="140%" height="180%" color-interpolation-filters="sRGB">
      <feTurbulence type="fractalNoise" baseFrequency="0.012 0.34" numOctaves="3" seed="23" result="grn13noise"/>
      <feDisplacementMap in="SourceGraphic" in2="grn13noise" scale="18" xChannelSelector="R" yChannelSelector="G"/>
    </filter>
  </svg>

  <div class="grn-13__theme">
    <input class="grn-13__theme-input" type="checkbox" id="grn-13-theme">
    <label class="grn-13__theme-label" for="grn-13-theme">
      <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="8.5" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M12 3.5a8.5 8.5 0 0 0 0 17Z" fill="currentColor"/></svg>
      <span class="grn-13__sr">Toggle dark theme</span>
    </label>
  </div>

  <section class="grn-13__band grn-13__band--top" aria-labelledby="grn-13-title">
    <div class="grn-13__inner">
      <p class="grn-13__kicker">Field guide · issue 12</p>
      <h1 class="grn-13__h1" id="grn-13-title">Sections should meet like materials, not like divs</h1>
      <p class="grn-13__lede">Below this paragraph the edge is torn by an SVG displacement map, and the grain band runs straight across the join. That continuity is what makes it read as one surface.</p>
    </div>
    <span class="grn-13__tear" aria-hidden="true"></span>
    <span class="grn-13__band-grain" aria-hidden="true"></span>
  </section>

  <section class="grn-13__band grn-13__band--mid" aria-labelledby="grn-13-h-fade">
    <span class="grn-13__band-grain" aria-hidden="true"></span>
    <div class="grn-13__inner">
      <h2 class="grn-13__h2" id="grn-13-h-fade">The grained fade</h2>
      <p class="grn-13__body">The workhorse. A <code>linear-gradient</code> mask dissolves the section boundary over 8rem while the grain layer continues at identical weight on both sides. Use it in long documents where you want sections to breathe without adding visual events. It costs one mask and no extra DOM.</p>
      <ul class="grn-13__facts">
        <li><b>Mask</b><span>linear-gradient, 8rem</span></li>
        <li><b>Grain</b><span>continuous, 0.05</span></li>
        <li><b>DOM added</b><span>none</span></li>
      </ul>
    </div>
    <span class="grn-13__fade" aria-hidden="true"></span>
  </section>

  <section class="grn-13__band grn-13__band--bottom" aria-labelledby="grn-13-h-rule">
    <span class="grn-13__band-grain" aria-hidden="true"></span>
    <div class="grn-13__inner">
      <h2 class="grn-13__h2" id="grn-13-h-rule">The ruled divider that does not look like a border</h2>
      <p class="grn-13__body">A hairline with a grain wash spilling past it in both directions. Utilitarian, appropriate in dashboards and documentation, and the only one of the three that survives being used a dozen times on one page.</p>
      <p class="grn-13__foot-note">Pick one divider per page. This demo shows all three because it is a specimen sheet — shipping all three would be a design failure, not a feature.</p>
    </div>
  </section>
</section>
```
## CSS
```css
.grn-13 {
  --grn-13-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='170' height='170'%3E%3Cfilter id='grn13n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='170' height='170' filter='url(%23grn13n)'/%3E%3C/svg%3E");
  --grn-13-top: #e9ecdf;
  --grn-13-top: oklch(93% 0.02 120);
  --grn-13-mid: #47543a;
  --grn-13-mid: oklch(42% 0.05 130);
  --grn-13-bottom: #f4f5ee;
  --grn-13-bottom: oklch(96% 0.01 120);
  --grn-13-ink: #1f2418;
  --grn-13-ink-on-mid: #eef2e4;
  --grn-13-mut: #616a55;
  --grn-13-acc: #7f9b3f;
  --grn-13-acc: oklch(62% 0.13 128);
  --grn-13-grain: 0.05;
  --grn-13-tear-h: clamp(3rem, 8vw, 6rem);
  --grn-13-font-display: "Space Grotesk", "Inter Tight", ui-sans-serif, system-ui, sans-serif;
  --grn-13-font-mono: ui-monospace, "SFMono-Regular", "JetBrains Mono", Menlo, monospace;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  position: relative;
  box-sizing: border-box;
  background: var(--grn-13-bottom);
  color: var(--grn-13-ink);
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  line-height: 1.6;
}

.grn-13 *,
.grn-13 *::before,
.grn-13 *::after {
  box-sizing: border-box;
}

.grn-13 :focus-visible {
  outline: 2px solid var(--grn-13-acc);
  outline-offset: 3px;
}

.grn-13 code {
  font-family: var(--grn-13-font-mono);
  font-size: 0.9em;
}

.grn-13__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.grn-13__defs {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.grn-13:has(.grn-13__theme-input:checked) {
  --grn-13-top: #171a13;
  --grn-13-top: oklch(20% 0.02 125);
  --grn-13-mid: #2b3324;
  --grn-13-mid: oklch(28% 0.04 130);
  --grn-13-bottom: #101208;
  --grn-13-bottom: oklch(16% 0.02 118);
  --grn-13-ink: #e8eddd;
  --grn-13-ink-on-mid: #e8eddd;
  --grn-13-mut: #9aa68c;
  --grn-13-acc: #b6d16a;
  --grn-13-acc: oklch(83% 0.15 122);
  --grn-13-grain: 0.075;
}

@media (prefers-color-scheme: dark) {
  .grn-13 {
    --grn-13-top: #171a13;
    --grn-13-top: oklch(20% 0.02 125);
    --grn-13-mid: #2b3324;
    --grn-13-mid: oklch(28% 0.04 130);
    --grn-13-bottom: #101208;
    --grn-13-bottom: oklch(16% 0.02 118);
    --grn-13-ink: #e8eddd;
    --grn-13-ink-on-mid: #e8eddd;
    --grn-13-mut: #9aa68c;
    --grn-13-acc: #b6d16a;
    --grn-13-acc: oklch(83% 0.15 122);
    --grn-13-grain: 0.075;
  }

  .grn-13:has(.grn-13__theme-input:checked) {
    --grn-13-top: #e9ecdf;
    --grn-13-top: oklch(93% 0.02 120);
    --grn-13-mid: #47543a;
    --grn-13-mid: oklch(42% 0.05 130);
    --grn-13-bottom: #f4f5ee;
    --grn-13-bottom: oklch(96% 0.01 120);
    --grn-13-ink: #1f2418;
    --grn-13-ink-on-mid: #eef2e4;
    --grn-13-mut: #616a55;
    --grn-13-acc: #7f9b3f;
    --grn-13-acc: oklch(62% 0.13 128);
    --grn-13-grain: 0.05;
  }
}

@media (prefers-color-scheme: light) {
  .grn-13:has(.grn-13__theme-input:checked) {
    --grn-13-top: #171a13;
    --grn-13-top: oklch(20% 0.02 125);
    --grn-13-mid: #2b3324;
    --grn-13-mid: oklch(28% 0.04 130);
    --grn-13-bottom: #101208;
    --grn-13-bottom: oklch(16% 0.02 118);
    --grn-13-ink: #e8eddd;
    --grn-13-ink-on-mid: #e8eddd;
    --grn-13-mut: #9aa68c;
    --grn-13-acc: #b6d16a;
    --grn-13-acc: oklch(83% 0.15 122);
    --grn-13-grain: 0.075;
  }
}

.grn-13__theme {
  position: absolute;
  inset-block-start: clamp(1rem, 3vw, 1.75rem);
  inset-inline-end: clamp(0.85rem, 3vw, 1.75rem);
  z-index: 60;
}

.grn-13__theme-input {
  position: absolute;
  inset: 0;
  opacity: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  cursor: pointer;
}

.grn-13__theme-label {
  display: grid;
  place-items: center;
  inline-size: 44px;
  block-size: 44px;
  border: 1px solid color-mix(in oklab, var(--grn-13-ink) 22%, transparent);
  border-radius: 999px;
  background: var(--grn-13-bottom);
  color: var(--grn-13-ink);
  cursor: pointer;
}

.grn-13__theme-label svg {
  inline-size: 20px;
  block-size: 20px;
}

.grn-13__band {
  position: relative;
  isolation: isolate;
}

.grn-13__band--top {
  background: var(--grn-13-top);
  padding-block: clamp(3.5rem, 9vw, 6rem) calc(var(--grn-13-tear-h) + clamp(2rem, 5vw, 3.5rem));
}

.grn-13__band--mid {
  background: var(--grn-13-mid);
  color: var(--grn-13-ink-on-mid);
  padding-block: clamp(3rem, 8vw, 5rem) clamp(5rem, 12vw, 8rem);
  margin-block-start: calc(var(--grn-13-tear-h) * -1);
}

.grn-13__band--bottom {
  background: var(--grn-13-bottom);
  padding-block: clamp(3rem, 8vw, 5rem) clamp(3.5rem, 9vw, 6rem);
}

.grn-13__band--bottom::before {
  content: "";
  display: block;
  block-size: 1px;
  inline-size: min(64rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
  margin: 0 auto clamp(2rem, 5vw, 3rem);
  background: color-mix(in oklab, var(--grn-13-ink) 20%, transparent);
}

.grn-13__band-grain {
  position: absolute;
  inset: calc(var(--grn-13-tear-h) * -1) 0;
  z-index: 1;
  pointer-events: none;
  background-image: var(--grn-13-noise);
  opacity: var(--grn-13-grain);
  mix-blend-mode: overlay;
}

.grn-13__tear {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 2;
  block-size: var(--grn-13-tear-h);
  background: var(--grn-13-top);
  filter: url(#grn-13-tear);
  pointer-events: none;
}

.grn-13__fade {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 2;
  block-size: clamp(5rem, 12vw, 8rem);
  background: var(--grn-13-bottom);
  pointer-events: none;
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 82%);
  mask-image: linear-gradient(to bottom, transparent, #000 82%);
}

.grn-13__inner {
  position: relative;
  z-index: 3;
  inline-size: min(64rem, calc(100% - clamp(1.5rem, 5vw, 4rem)));
  margin-inline: auto;
  display: grid;
  gap: 0.75rem;
  max-inline-size: 46rem;
}

.grn-13__kicker {
  margin: 0;
  font: 500 0.72rem/1 var(--grn-13-font-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--grn-13-acc);
}

.grn-13__h1 {
  margin: 0;
  font: 600 clamp(2rem, 5.6vw, 3.5rem)/1.03 var(--grn-13-font-display);
  letter-spacing: -0.035em;
  text-wrap: balance;
}

.grn-13__h2 {
  margin: 0;
  font: 600 clamp(1.4rem, 3.6vw, 2.1rem)/1.1 var(--grn-13-font-display);
  letter-spacing: -0.03em;
  text-wrap: balance;
}

.grn-13__lede {
  margin: 0;
  max-inline-size: 40rem;
  color: var(--grn-13-mut);
  font-size: clamp(0.97rem, 2vw, 1.1rem);
  text-wrap: pretty;
}

.grn-13__body {
  margin: 0;
  max-inline-size: 42rem;
  font-size: clamp(0.95rem, 1.9vw, 1.05rem);
  text-wrap: pretty;
  color: color-mix(in oklab, currentColor 82%, transparent);
}

.grn-13__facts {
  list-style: none;
  margin: 0.4rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.8rem;
}

.grn-13__facts li {
  display: grid;
  gap: 0.1rem;
}

.grn-13__facts b {
  font: 600 0.8rem/1.2 var(--grn-13-font-display);
}

.grn-13__facts span {
  font: 400 0.78rem/1.2 var(--grn-13-font-mono);
  opacity: 0.75;
}

.grn-13__foot-note {
  margin: 0.5rem 0 0;
  padding-block-start: 0.8rem;
  border-block-start: 1px solid color-mix(in oklab, var(--grn-13-ink) 16%, transparent);
  color: var(--grn-13-mut);
  font-size: 0.85rem;
  text-wrap: pretty;
}

@media (prefers-reduced-motion: reduce) {
  .grn-13 * {
    transition-duration: 1ms !important;
    animation-duration: 1ms !important;
  }
}
```

How this works

Why a hard section edge looks cheap. Physical materials do not meet in perfect straight lines unless they were cut by a machine, and your eye knows it. A 1px boundary between two colours is read as "two divs" rather than "two surfaces". Print design solves this with deckle edges, torn paper, misregistration and bleed. The web equivalent is a mask with noise in it.

feDisplacementMap is the tearing engine. Take a hard-edged shape, generate turbulence, and use the turbulence to displace the shape's pixels. The scale attribute controls how violent the tear is: 6 is a subtle deckle, 24 is aggressively torn paper. Because the displacement is procedural, the edge never repeats and never looks like a shape somebody drew — which is exactly the failure mode of the SVG "wave divider" generators.

Grain must cross the join. This is the detail that separates a good divider from a gimmick. If the top section is grained and the bottom section is flat, the tear reads as a cutout over two unrelated backgrounds. Put a single grain band across the transition zone, at the same weight on both sides, and the two sections read as one piece of material that has been torn — which is the whole point.

Three variants, three uses. The torn edge is for editorial and marketing moments where you want physicality. The grained fade is for long documents where you want sections to breathe without visual noise — it is a gradient mask plus grain, and it is the one you will use most. The ruled divider with a grain wash is the utilitarian option: a hairline that does not look like a border because the texture continues past it. Pick one per page; using all three, as this demo deliberately does, is a specimen sheet, not a design.

Make it yours

  • Change the displacement scale from 18 to tune tear violence — under 8 for deckle, over 30 for shredded.
  • Change the turbulence baseFrequency on the displacement map to alter the tear's rhythm: low frequency gives long ragged rips, high gives fine fraying.
  • Swap the mask's linear-gradient stops to move where the fade begins and ends.
  • Set the divider height with --grn-13-tear-h; taller transitions read softer at the same displacement scale.
  • Remove the grain band for a clean tear if your sections are photographic rather than flat.
  • Recolour with the two section tokens; the mask and grain are colour-agnostic.

Gotchas — read before shipping

  • mask-image with an SVG reference needs -webkit-mask-image for older Safari. The demo declares both; omitting the prefix means the mask is ignored and the section shows a hard rectangle.
  • A displaced edge extends beyond the original shape's bounds, so the filter region must be enlarged (x="-20%" width="140%") or the tear gets clipped flat.
  • Grain that stops at the join makes the tear look like a cutout. The band has to cross it.
  • Decorative dividers with text content inside them are an accessibility bug waiting to happen. Keep them empty and aria-hidden.
  • Very tall torn dividers at full viewport width are a large filter region to evaluate. Cap the height, and never animate the displacement scale.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by :has(), oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

feDisplacementMap and mask-image are SVG 1.1 and CSS Masking Level 1 respectively, both widely supported; Safari needs the -webkit- prefix for mask shorthand properties on older versions, which the demo ships. mask-composite is Chrome 120+, Safari 15.4+ and Firefox 53+; where it is missing the fade still applies without the tear combination. The version floor is oklch() and :has().

Techniques used in this demo

Search CodeFronts

Loading…