Dividers

beta

Section dividers create intentional transitions between page sections — signalling a shift in content, surface depth, or visual energy. Farn ships pure-CSS and SVG-backed patterns that all consume the semantic token layer, so transitions stay coherent as the palette evolves.

Three patterns are currently shipped: layered overlap (stable), sine wave (beta), and convex arc (beta). Additional patterns (organic blob, diagonal cut, stacked card reveal) are planned. Use one primary divider type per page; introduce a second only for a single high-emphasis moment.

Layered overlap

The only pure-CSS divider — no SVG required. The next section or a card bleeds upward into the preceding section's space using a negative margin-top. Combined with border-radius on the top corners, it creates a floating-panel illusion that implies z-axis depth.

Option A — full section overlap

The entire next section slides up over the preceding one. Use for major content transitions such as a dark hero giving way to a light content area.

Dark hero or feature area. No overflow: hidden here.

Rounded top corners float over the section above. Consumer sets background.

<section class="overlap-preceding hero-section">
  <!-- Hero content —  never add overflow:hidden to this element -->
</section>

<section class="overlap-section" style="background: var(--color-bg-panel);">
  <!-- Content that slides up over the hero -->
</section>

Option B — card floats up

A card bleeds upward from one section into the section above. Add data-scroll-reveal to trigger the entry animation — the card rises with a growing shadow on first scroll-into-view.

Dark area — the card will float up into this space.

Rises with a shadow as it enters view. Background: --card-bg. Negative margin-top bleeds into the section above.

<section class="overlap-preceding">
  <!-- Content that precedes the floating card -->
</section>

<div class="overlap-card" data-scroll-reveal>
  <!-- Card content -->
</div>

Anatomy

The pattern uses two or three elements: the preceding section (where the overlap visually occurs) and the overlapping element itself.

Class / attributeElementPurpose
.overlap-preceding Any block Applied to the section before the overlapping element. Sets overflow: visible (the browser default, but made explicit to prevent accidental overflow: hidden which clips the overlapping child). Also adds padding-bottom to reserve space before the negative margin takes effect.
.overlap-section <section>, <div> Option A — the full section that slides up. Sets z-index: 10, negative margin-top, and rounded top corners. Consumer sets background — the class does not prescribe it.
.overlap-card <div>, <article> Option B — a card that floats upward. Sets background: var(--card-bg), negative margin-top, and Tier-1 entry animation (opacity + translateY + box-shadow grow).
data-scroll-reveal .overlap-card Opts the card into the shared IntersectionObserver from scroll-reveal.js. The observer adds .is-visible on first entry, triggering the CSS transition. Omit if the card should always be visible (e.g., above the fold).

Critical: overflow on the preceding element

overflow: hidden on .overlap-preceding (or any of its ancestors) will clip the overlapping element completely. .overlap-preceding makes the intent explicit, but ensure no ancestor in the stacking context also sets overflow: hidden.

Token reference

TokenDefaultPurpose
--overlap-section-radiusvar(--radius-xl)Top corner radius on .overlap-section
--overlap-section-offset60pxHow far .overlap-section bleeds upward
--overlap-card-radiusvar(--card-radius)Border radius on .overlap-card — inherits from card tokens
--overlap-card-offset80pxHow far .overlap-card bleeds upward
--overlap-card-duration0.7sEntry animation duration — longer than standard reveals to match the physical lift metaphor
--overlap-card-shadow-raisedLight: rgba(0,0,0,0.14) · Dark: rgba(0,0,0,0.40)Shadow in the raised state. Deepened in dark mode where light shadows are invisible.

Overriding tokens

/* Shallower overlap for a subtle transition */
.my-section {
  --overlap-section-offset: 32px;
}

/* More dramatic card lift */
.hero-cta {
  --overlap-card-offset: 120px;
  --overlap-card-duration: 0.9s;
}

CSS reference

Shipped in dist/farn-components.css. Load alongside farn.css or farn-tokens.css.

/* Guardrail — makes overflow:visible intent explicit */
.overlap-preceding {
  overflow: visible;
  padding-bottom: var(--space-lg);
}

/* Option A — full section overlap */
.overlap-section {
  position: relative;
  z-index: 10;
  margin-top: calc(-1 * var(--overlap-section-offset));
  border-radius: var(--overlap-section-radius) var(--overlap-section-radius) 0 0;
  padding-top: calc(var(--overlap-section-offset) + var(--space-xl));
  /* consumer sets background */
}

/* Option B — card floats up */
.overlap-card {
  position: relative;
  z-index: 10;
  margin-top: calc(-1 * var(--overlap-card-offset));
  border-radius: var(--overlap-card-radius);
  background: var(--card-bg);
  padding: var(--card-padding);
  opacity: 0;
  transform: translateY(40px);
  box-shadow: 0 0 0 rgba(0, 0, 0, 0);
  transition-property: opacity, transform, box-shadow;
  transition-duration: var(--overlap-card-duration);
  transition-timing-function: var(--ease-out);
  transition-delay: 0.05s;
}

.overlap-card.is-visible {
  opacity: 1;
  transform: translateY(0);
  box-shadow: var(--overlap-card-shadow-raised);
}

Sine wave

An SVG divider using cubic bezier curves. Two layered paths — a depth layer behind and a main wave on top — create the illusion of a receding tide. The most versatile and character-appropriate pattern in Farn: organic, rhythmic, suited to any content transition.

The wave sits absolutely positioned at the bottom of its parent section. It fills in the next section's background colour — set --color-section-next on the parent section to match the actual destination background.

Wave sits at the bottom — fill matches the section below.

Override --color-section-next to match this background.

<section style="position: relative; padding-bottom: var(--wave-height);
              --color-section-next: var(--color-bg-panel);">
  <!-- Section content -->
  <div class="section-wave" data-scroll-reveal aria-hidden="true">
    <svg viewBox="0 0 1200 80" preserveAspectRatio="none">
      <!-- depth layer first (SVG paints in document order) -->
      <path d="M0,40 C300,80 900,0 1200,40 L1200,80 L0,80 Z"
            fill="var(--color-section-next)" opacity="0.4"/>
      <path d="M0,45 C300,85 900,5 1200,45 L1200,80 L0,80 Z"
            fill="var(--color-section-next)"/>
    </svg>
  </div>
</section>

Anatomy

Element / attributeRoleNotes
Parent <section> Wave host Must have position: relative so the wave positions against it, and padding-bottom: var(--wave-height) so content does not sit beneath the wave. Set --color-section-next here to match the destination section's background.
.section-wave SVG container Absolutely positioned at the bottom of the parent. Starts hidden (opacity: 0, translated down) and animates in when scrolled into view.
data-scroll-reveal Animation opt-in Registers the element with the shared IntersectionObserver. Adds .is-visible on first entry, triggering the rise animation. Omit if the section is above the fold.
aria-hidden="true" Accessibility The wave is purely decorative — hidden from assistive technology.
preserveAspectRatio="none" SVG scaling Essential. Without it the wave does not stretch to fill the full viewport width. The viewBox width (1200) is a coordinate space, not a rendered size.
Depth path (opacity="0.4") Depth layer Must come first in SVG source order — SVG paints in document order, so this renders behind the main wave. A slightly different Y offset creates the layered illusion.

Customisation

Change amplitude by adjusting the control point Y values. The paths use C300,80 900,0 — equal and opposite offsets produce a symmetric wave. Increase them for more drama; reduce for a subtler ripple.

To reverse direction (wave dips down rather than up), place .section-wave at the top of the next section and apply transform: scaleY(-1) to the .section-wave element. Do not use rotateX — it requires a 3D rendering context.

Token reference

TokenDefaultPurpose
--wave-height 80px SVG rendered height and the minimum padding-bottom the parent section needs. Override on the parent to scale the wave up or down — change one value, both stay in sync.
--color-section-next var(--color-bg-panel) Fill colour for both SVG paths. Set this on the parent section to match the actual destination background — the default is a reasonable fallback but will often be wrong.
/* Taller wave for a more dramatic transition */
.hero-section {
  --wave-height: 120px;
  padding-bottom: var(--wave-height);
  --color-section-next: var(--color-bg);
}

CSS reference

.section-wave {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  overflow: hidden;
  line-height: 0;
  opacity: 0;
  transform: translateY(24px);
  transition-property: opacity, transform;
  transition-duration: var(--duration-reveal);
  transition-timing-function: var(--ease-out);
}

.section-wave svg {
  display: block;
  width: 100%;
  height: var(--wave-height);
}

.section-wave.is-visible {
  opacity: 1;
  transform: translateY(0);
  transition-delay: 0.1s;
}

@media (prefers-reduced-motion: reduce) {
  .section-wave { opacity: 1; transform: none; transition: none; }
}

Convex arc

A single quadratic bezier curve spanning the full width. Calmer than the sine wave — one smooth dome rather than repeating crests — and naturally suited to centred hero layouts where the apex draws the eye to content at the peak.

The convex (dome) variant uses a Tier 2 scroll-driven animation: as the section enters the viewport the arc expands from a flat baseline to its full dome shape via animation-timeline: view(). Browsers without scroll timeline support, and those with prefers-reduced-motion set, show the full arc statically. The concave (bowl) variant is always static.

Convex — dome upward

Arc sits at the bottom — fill matches the section below.

Override --color-section-next to match this background.

<section style="position: relative; padding-bottom: var(--arc-height);
              --color-section-next: var(--color-bg-panel);">
  <!-- Section content -->
  <div class="section-arc" aria-hidden="true">
    <svg viewBox="0 0 1200 80" preserveAspectRatio="none">
      <path d="M0,80 Q600,0 1200,80 Z"
            fill="var(--color-section-next)"/>
    </svg>
  </div>
</section>

Concave — bowl upward

The concave variant redraws the path with the bezier control point below the baseline (Q600,80) and closes with explicit bottom corners. It does not use scaleY(-1) on the wrapper — that approach flips the fill colour in some compositing contexts. Add the .section-arc--concave modifier to disable the scroll animation (static only).

Bowl arc — fill still matches the section below.

<!-- Concave — redraw path with control point below baseline -->
<div class="section-arc section-arc--concave" aria-hidden="true">
  <svg viewBox="0 0 1200 80" preserveAspectRatio="none">
    <path d="M0,0 Q600,80 1200,0 L1200,80 L0,80 Z"
          fill="var(--color-section-next)"/>
  </svg>
</div>

Anatomy

Element / attributeRoleNotes
Parent <section> Arc host Must have position: relative and padding-bottom: var(--arc-height) so content does not sit beneath the arc. Set --color-section-next here to match the destination section's background.
.section-arc SVG container Absolutely positioned at the bottom of the parent. In supporting browsers the SVG starts as a flat line and expands to the full arc shape during scroll.
.section-arc--concave Concave modifier Disables the scroll animation — the concave arc is always fully visible. Use alongside a redrawn SVG path (Q600,80) rather than scaleY(-1) on the wrapper.
aria-hidden="true" Accessibility The arc is purely decorative — hidden from assistive technology.
preserveAspectRatio="none" SVG scaling Essential. Without it the arc does not stretch to fill the full viewport width. The viewBox width (1200) is a coordinate space, not a rendered size.

Customisation

Move the control point horizontally to create an asymmetric arc: Q400,0 instead of Q600,0 shifts the dome apex off-centre. Increase the viewBox height (0 0 1200 120) for a more dramatic curve without changing the SVG's rendered height.

For a subtle depth effect, add a second path at opacity="0.3" with the control point offset by ~10px: Q600,10. Put the depth path first in source order so SVG paints it behind the main arc.

Token reference

TokenDefaultPurpose
--arc-height 80px SVG rendered height and the padding-bottom the parent section needs. Override on the parent to scale the arc — change one value, both stay in sync.
--color-section-next var(--color-bg-panel) Fill colour for the arc. Set on the parent section to match the actual destination background — the default is a fallback and will often be wrong.
/* Taller arc for a more dramatic curve */
.hero-section {
  --arc-height: 120px;
  padding-bottom: var(--arc-height);
  --color-section-next: var(--color-bg);
}

CSS reference

Shipped in dist/farn-components.css. Load alongside farn.css or farn-tokens.css.

.section-arc {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  line-height: 0;
}

.section-arc svg {
  display: block;
  width: 100%;
  height: var(--arc-height);
  clip-path: polygon(0% 100%, 50% 100%, 100% 100%);
}

@supports (animation-timeline: view()) {
  @keyframes arc-expand {
    from { clip-path: polygon(0% 100%, 50% 100%, 100% 100%); }
    to   { clip-path: polygon(0% 100%, 50% 0%,   100% 100%); }
  }

  .section-arc svg {
    animation: arc-expand linear both;
    animation-timeline: view();
    animation-range: entry 20% cover 60%;
  }
}

@supports not (animation-timeline: view()) {
  .section-arc svg { clip-path: none; }
}

.section-arc--concave svg {
  clip-path: none;
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .section-arc svg { clip-path: none; animation: none; }
}

Usage guidance

Use one primary divider type per page. A second type is acceptable for one high-emphasis moment — for example, sine waves throughout with a single layered overlap at a CTA. More than two distinct types creates visual noise.

PatternCharacterBest for
Layered overlap Dimensional, modern Major theme transitions (dark hero → light content); featured CTA panels
Sine wave Organic, rhythmic Repeated section breaks throughout a page; any transition needing a natural, flowing feel
Convex arc Refined, quiet Centred hero layouts; transitions needing calm, lens-like elegance rather than repeating rhythm

Dark over light works best for both patterns. A dark preceding section with a lighter destination maximises the visual depth. The reverse works but is subtler.