Dividers
betaSection 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.
Preceding section
Dark hero or feature area. No overflow: hidden here.
Overlapping section
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.
Background section
Dark area — the card will float up into this space.
Floating card
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 / attribute | Element | Purpose |
|---|---|---|
.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
| Token | Default | Purpose |
|---|---|---|
--overlap-section-radius | var(--radius-xl) | Top corner radius on .overlap-section |
--overlap-section-offset | 60px | How far .overlap-section bleeds upward |
--overlap-card-radius | var(--card-radius) | Border radius on .overlap-card — inherits from card tokens |
--overlap-card-offset | 80px | How far .overlap-card bleeds upward |
--overlap-card-duration | 0.7s | Entry animation duration — longer than standard reveals to match the physical lift metaphor |
--overlap-card-shadow-raised | Light: 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.
Preceding section
Wave sits at the bottom — fill matches the section below.
Next section
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 / attribute | Role | Notes |
|---|---|---|
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
| Token | Default | Purpose |
|---|---|---|
--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
Preceding section
Arc sits at the bottom — fill matches the section below.
Next section
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).
Preceding section
Bowl arc — fill still matches the section below.
Next section
<!-- 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 / attribute | Role | Notes |
|---|---|---|
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
| Token | Default | Purpose |
|---|---|---|
--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.
| Pattern | Character | Best 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.