ns-ui
Welt Channel Close
A full-width section divider modeled on Goodyear-welted shoe construction: a needle locks one welt stitch at a time at a single working point while, a few stitches behind it, the channel flap that exposed the last batch of stitches folds shut on a real CSS 3D hinge and hides them for good. The lockstitch is never drawn; only a plain flush seam is ever visible once a flap closes.
Use when Pick welt-channel-close when the divider's mechanic is fold-and-conceal — a discrete working point advancing stitch by stitch while a trailing flap physically hinges shut to permanently hide what was just done, rendered as real DOM elements with a CSS 3D rotateX transform. Pick bobbin-lace-pricking instead when the divider should keep every crossing visible and the mechanic is pin-pull — a temporary pin fixture removed once its (still-visible) thread crossing has secured, rendered on canvas; that component never conceals anything, it only removes the pin. Pick divider-telephone-cord-delam instead when the mechanic is a compressive failure front wandering and re-forking through an already-formed film, with no discrete per-stitch events at all.
Install
npx shadcn add https://design.helpmarq.com /r/welt-channel-close.jsonSource
registry/core/welt-channel-close/component.tsx"use client";
import { useEffect, useRef } from "react";
// ---------------------------------------------------------------------------
// WeltChannelClose — a full-width section divider modeled on Goodyear-welted
// shoe construction. A channel is skived into the insole rib to expose it; a
// curved awl and needle lockstitch a welt to the upper and insole rib through
// that open channel; immediately behind the working point, the lifted channel
// flap that exposed the rib is folded back down and pressed flush, PERMANENTLY
// CONCEALING the just-completed stitching under the insole surface. The
// outer edge only ever shows a plain seam — the lockstitch itself is hidden
// the moment it is finished.
//
// This is deliberately NOT a restyle of bobbin-lace-pricking (pin-pull):
// that component draws every crossing thread and removes a placed PIN once
// its crossing has secured, on canvas. This component never draws a thread
// crossing at all — the lockstitch is conceptually always hidden inside the
// channel — and what the viewer actually sees is a real DOM/CSS 3D hinge
// transform (rotateX) folding a physical FLAP shut over the seam, plus a
// needle that pokes and withdraws at a single working point. Different
// mechanic (fold-and-conceal vs. pin-pull), different visible event (a
// hinge closing vs. a thread crossing + pin sliding out), different
// rendering technique (real DOM elements + CSS custom properties vs.
// canvas 2D draw calls).
//
// TIMELINE — one continuous clock, no per-stitch React state:
// currentStitchFloat = elapsedMs / STITCH_INTERVAL_MS (smooth fractional
// stitch depth)
// currentIndex = floor(currentStitchFloat) (stitch the
// needle is
// actively locking)
// feedDistance = currentStitchFloat * CELL_SPACING (px the whole
// strip has fed
// left, in lockstep
// with the stitch
// count so the
// working point
// never drifts more
// than one cell)
//
// Every stitch position i is classified purely by ageMs = elapsedMs -
// i * STITCH_INTERVAL_MS (can be negative — a stitch not yet reached):
// ageMs < FLAP_LAG_MS flap open (channel skived, not yet
// worked, or just locked and still
// waiting its FLAP_LAG turn) — lift = 1
// FLAP_LAG_MS <= ageMs < +FOLD_MS the fold itself: an eased rotateX
// hinge from lift=1 to lift=0 over
// FOLD_MS, transform-origin on the
// trailing (already-closed) edge
// ageMs >= FLAP_LAG_MS + FOLD_MS flush — folded flat, permanently
// closed, drawn only as part of the single
// continuous seam hairline behind it
//
// Because ageMs is linear in i, the boundary between "still open" and
// "already flush" trails the working point by a FIXED number of stitches
// ((FLAP_LAG_MS + FOLD_MS) / STITCH_INTERVAL_MS), so the flush seam's
// length is a single constant offset from the working point's screen
// position — computed once per frame, not accumulated or re-measured.
// ---------------------------------------------------------------------------
const CELL_SPACING = 14; // px per stitch — ~6-7 stitches/inch welt gauge at card DPI
const STITCH_INTERVAL_MS = 1050; // one lockstitch, comfortably above the 1s legibility floor
const FLAP_LAG_STITCHES = 3; // a flap stays open until its stitch is this many positions old
const FLAP_LAG_MS = FLAP_LAG_STITCHES * STITCH_INTERVAL_MS;
const FOLD_MS = 260; // fold transition duration — explicit hinge, never a blink
const NEEDLE_MS = 420; // needle in -> lock -> out, timed inside each stitch's own interval
const WORKING_X_FRAC = 0.45; // working point's fixed screen fraction across the strip
const POOL_SIZE = 200; // generous fixed DOM pool; covers dividers well past 2000px wide
const STATIC_STITCH_INDEX = 8; // reduced-motion: which stitch's crossing to freeze mid-lock
const STATIC_T_MS = STATIC_STITCH_INDEX * STITCH_INTERVAL_MS + NEEDLE_MS / 2; // MID_LOCK
function easeOutCubic(x: number): number {
const t = 1 - x;
return 1 - t * t * t;
}
export interface WeltChannelCloseProps {
/** band height in px. Flap/needle size derive from this, the strip's own smaller dimension. Default 44. */
height?: number;
className?: string;
}
export function WeltChannelClose({ height = 44, className = "" }: WeltChannelCloseProps) {
const wrapRef = useRef<HTMLDivElement>(null);
const seamRef = useRef<HTMLDivElement>(null);
const needleRef = useRef<HTMLDivElement>(null);
const flapRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
const wrap = wrapRef.current;
const seam = seamRef.current;
const needle = needleRef.current;
if (!wrap || !seam || !needle) return;
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let width = 0;
let sized = false;
let visible = true;
let raf = 0;
const resize = () => {
const rect = wrap.getBoundingClientRect();
width = rect.width;
sized = width >= 2;
};
// -- render is a pure function of elapsedMs: nothing here is persisted
// per-stitch state, so re-indexing which pool slot represents which
// stitch (as the visible window slides) never loses or restarts an
// in-flight fold. ------------------------------------------------------
const render = (elapsedMs: number) => {
if (!sized) return;
const currentStitchFloat = elapsedMs / STITCH_INTERVAL_MS;
const currentIndex = Math.floor(currentStitchFloat);
const feedDistance = currentStitchFloat * CELL_SPACING;
const workingX = width * WORKING_X_FRAC;
const iMin = Math.floor((feedDistance - workingX) / CELL_SPACING) - 1;
const iMax = Math.ceil((feedDistance + (width - workingX)) / CELL_SPACING) + 1;
const visibleCount = Math.min(POOL_SIZE, Math.max(0, iMax - iMin + 1));
for (let p = 0; p < POOL_SIZE; p++) {
const el = flapRefs.current[p];
if (!el) continue;
if (p >= visibleCount) {
el.style.display = "none";
continue;
}
const i = iMin + p;
const screenX = workingX + i * CELL_SPACING - feedDistance;
const ageMs = elapsedMs - i * STITCH_INTERVAL_MS;
let lift: number;
if (ageMs < FLAP_LAG_MS) {
lift = 1;
} else if (ageMs < FLAP_LAG_MS + FOLD_MS) {
const foldProgress = (ageMs - FLAP_LAG_MS) / FOLD_MS;
lift = 1 - easeOutCubic(foldProgress);
} else {
lift = 0;
}
el.style.display = "block";
el.style.transform = `translateX(${screenX.toFixed(2)}px)`;
el.style.setProperty("--lift", lift.toFixed(4));
}
// flush seam: constant lag behind the working point (derived in the
// header comment), so its length needs no per-stitch loop at all.
const boundaryOffset = (CELL_SPACING * (FLAP_LAG_MS + FOLD_MS)) / STITCH_INTERVAL_MS;
const seamWidth = Math.max(0, workingX - boundaryOffset);
seam.style.width = `${seamWidth.toFixed(2)}px`;
// needle: only present during its own stitch's 420ms crossing window,
// drifting with the feed the same as every other stitch position.
const needleLocalMs = elapsedMs - currentIndex * STITCH_INTERVAL_MS;
const needleActive = needleLocalMs <= NEEDLE_MS;
const needleX = workingX + currentIndex * CELL_SPACING - feedDistance;
const depth = needleActive ? Math.sin((Math.min(1, needleLocalMs / NEEDLE_MS)) * Math.PI) : 0;
needle.style.transform = `translateX(${needleX.toFixed(2)}px) scaleY(${(0.25 + 0.75 * depth).toFixed(3)})`;
needle.style.opacity = needleActive ? "1" : "0";
};
let startTime = 0;
const loop = (now: number) => {
if (!startTime) startTime = now;
render(now - startTime);
if (visible && !document.hidden) raf = requestAnimationFrame(loop);
};
const start = () => {
if (reduced) {
render(STATIC_T_MS);
return;
}
startTime = 0;
cancelAnimationFrame(raf);
raf = requestAnimationFrame(loop);
};
let resizeTimer: ReturnType<typeof setTimeout> | null = null;
const onResize = () => {
if (resizeTimer) clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
resizeTimer = null;
resize();
if (reduced) render(STATIC_T_MS);
}, 120);
};
const ro = new ResizeObserver(onResize);
ro.observe(wrap);
const io = new IntersectionObserver(
(entries) => {
visible = entries[0]?.isIntersecting ?? true;
if (visible && !document.hidden && !reduced) {
cancelAnimationFrame(raf);
raf = requestAnimationFrame(loop);
}
},
{ threshold: 0 },
);
io.observe(wrap);
const onVis = () => {
cancelAnimationFrame(raf);
if (!document.hidden && visible && !reduced) raf = requestAnimationFrame(loop);
};
document.addEventListener("visibilitychange", onVis);
resize();
start();
return () => {
cancelAnimationFrame(raf);
if (resizeTimer) clearTimeout(resizeTimer);
ro.disconnect();
io.disconnect();
document.removeEventListener("visibilitychange", onVis);
};
}, []);
const flapSize = Math.max(10, height * 0.5);
return (
<div
ref={wrapRef}
role="separator"
aria-orientation="horizontal"
className={`ns-wcc relative w-full overflow-hidden ${className}`}
style={{ height, perspective: 260 }}
>
<style>{`
.ns-wcc-flap {
position: absolute;
top: 50%;
width: ${CELL_SPACING}px;
height: ${flapSize}px;
margin-top: ${-flapSize / 2}px;
transform-style: preserve-3d;
transform-origin: right center;
border-right: 1px solid var(--border);
background-color: color-mix(in srgb, var(--background), var(--foreground) calc(var(--lift, 0) * 5%));
box-shadow: 0 calc(var(--lift, 0) * 2px) calc(var(--lift, 0) * 6px)
color-mix(in srgb, var(--foreground) calc(var(--lift, 0) * 24%), transparent);
transform: translateX(0) perspective(260px) rotateX(calc(var(--lift, 0) * -32deg));
will-change: transform;
}
`}</style>
<div className="ns-wcc-seam absolute left-0 top-1/2 -translate-y-1/2 bg-foreground" ref={seamRef} style={{ height: 1, width: 0 }} />
{Array.from({ length: POOL_SIZE }).map((_, p) => (
<div
key={p}
ref={(el) => {
flapRefs.current[p] = el;
}}
className="ns-wcc-flap"
style={{ display: "none" }}
/>
))}
<div
ref={needleRef}
aria-hidden="true"
className="absolute left-0 top-1/2 bg-foreground"
style={{ width: 1, height: flapSize * 0.7, marginTop: -(flapSize * 0.35), opacity: 0 }}
/>
</div>
);
}
Build spec
Build <WeltChannelClose height? className?> as a drop-in replacement for <hr>/border-top between page sections, rendered as a <div role="separator" aria-orientation="horizontal"> holding a pool of real absolutely-positioned DOM elements (no canvas, no SVG) plus one CSS custom-property-driven <style> block. SOURCE: Goodyear-welted shoe construction — a channel is skived into the insole rib to expose it; a curved awl and needle lockstitch a welt to the upper and insole rib through that open channel; immediately behind the working point, the lifted channel flap that exposed the rib is folded back down and pressed flush, permanently concealing the just-completed stitching under the insole surface, so the outer edge only ever shows a plain seam. TIMELINE: one continuous clock, elapsedMs from mount, currentStitchFloat = elapsedMs / STITCH_INTERVAL_MS (STITCH_INTERVAL_MS = 1050, comfortably above the 1s legibility floor), currentIndex = floor(currentStitchFloat) is the stitch the needle is actively locking, feedDistance = currentStitchFloat * CELL_SPACING (CELL_SPACING = 14px, ~6-7 stitches/inch welt gauge at card DPI) is how far the whole strip has fed left — deliberately locked in lockstep with the stitch count (not a separate speed constant) so the working point never drifts more than one cell width. The working point sits at a FIXED screen fraction workingX = width * 0.45; any stitch index i's screen x = workingX + i*CELL_SPACING - feedDistance, continuously sliding left as elapsedMs advances (material scrolls off the left edge forever, an unbounded loop). CLASSIFY EVERY STITCH POSITION PURELY BY ageMs = elapsedMs - i*STITCH_INTERVAL_MS (negative for a stitch not yet reached), no per-stitch persisted React state: ageMs < FLAP_LAG_MS (FLAP_LAG_STITCHES=3, FLAP_LAG_MS=3150) keeps its flap fully lifted (CSS custom property --lift=1) — this single condition covers both a not-yet-worked stitch (channel already skived open ahead of the needle) and a just-locked stitch still waiting its turn; FLAP_LAG_MS <= ageMs < FLAP_LAG_MS+FOLD_MS (FOLD_MS=260) is the fold itself, --lift eased from 1 to 0 via easeOutCubic over that 260ms window — an explicit continuous rotateX hinge (transform: perspective(260px) rotateX(calc(var(--lift,0) * -32deg)), transform-origin: right center, the trailing/already-closed edge is the hinge line), never an opacity blink; ageMs >= FLAP_LAG_MS+FOLD_MS is flush-closed, --lift=0, and that flap element is simply hidden behind the single continuous seam hairline described below — the lockstitch itself is never drawn at any point, matching the real mechanic where it's concealed the instant it's finished. Because ageMs is linear in i, the boundary between still-open and already-flush trails the working point by a FIXED, precomputed number of stitches ((FLAP_LAG_MS+FOLD_MS)/STITCH_INTERVAL_MS), so the flush seam's length is one constant px offset from workingX recomputed once per frame (seamWidth = workingX - CELL_SPACING*(FLAP_LAG_MS+FOLD_MS)/STITCH_INTERVAL_MS, clamped >=0) rather than a per-stitch loop — the seam is a single absolutely-positioned div with only its width animated, height 1px, background var(--foreground) via the bg-foreground utility class (a genuine hairline stroke, never --border, which is a separator token invisible as a fill/stroke in light theme). FLAP POOL: a fixed pool of 200 pre-rendered divs (POOL_SIZE), each frame the visible index window [iMin,iMax] is recomputed from feedDistance/workingX/width and mapped onto pool slots 0..count-1 (slots beyond count get display:none); because nothing here relies on a stitch's DOM node identity persisting (--lift is a pure function of (i, elapsedMs), not accumulated), reassigning which stitch a pool slot represents as the window slides never restarts or loses an in-flight fold. Each flap div is CELL_SPACING wide, height = max(10, height*0.5) (derived from the strip's own smaller dimension, the height prop), background color-mix(in srgb, var(--background), var(--foreground) calc(var(--lift,0)*5%)) for a faint tonal lift, and box-shadow 0 calc(var(--lift,0)*2px) calc(var(--lift,0)*6px) color-mix(in srgb, var(--foreground) calc(var(--lift,0)*24%), transparent) for the lifted-edge cast — the ONLY cue distinguishing open from flush in both themes, verified to survive light theme where shadow contrast is naturally weaker (no hue change, luminance/alpha only). NEEDLE: one element (never pooled), present only during its own stitch's NEEDLE_MS=420ms crossing window inside each 1050ms interval (needleLocalMs = elapsedMs - currentIndex*STITCH_INTERVAL_MS, active when <= 420), depth = sin(clamp(needleLocalMs/420,0,1) * PI) drives scaleY(0.25 + 0.75*depth) for an in-lock-out poke and opacity toggles 1/0 with activity; rendered in --foreground via the bg-foreground utility class ONLY — --ns-accent must never appear anywhere in this component, this is an ambient divider with no interaction chrome. LEGIBILITY: the eye follows the single working point where the needle currently pokes, once every 1.05s (well above the 1s floor); three stitches behind it, the flap that was open folds flush over 260ms with a visible eased lift-to-flush hinge arc, giving a fast anchor (needle) and a slower confirmation event (fold) to track, exactly as specified. HOST: ResizeObserver on the wrapper (not window.resize, 120ms debounce) remeasures width, a single requestAnimationFrame loop paused via IntersectionObserver (threshold 0) when scrolled offscreen and via visibilitychange when the tab is hidden, full cleanup (cancelAnimationFrame, disconnect both observers, remove the visibilitychange listener) on unmount. REDUCED MOTION: prefers-reduced-motion renders exactly once at a fixed elapsedMs = 8*STITCH_INTERVAL_MS + NEEDLE_MS/2 (MID_LOCK) — the needle frozen mid-crossing (depth=1, fully inserted) with the full FLAP_LAG=3 window of lifted flaps visible ahead of the working point and the flush closed seam clearly visible behind it, so open/closing/closed all read in one static frame; no rAF loop starts. TOKENS: every color is a CSS custom-property reference (var(--foreground), var(--background), var(--border)) via Tailwind's token-backed utility classes (bg-foreground) and inline color-mix() expressions in the embedded <style> block — zero JS color reads, zero literals, zero hex, so both themes and any live token change repaint for free through the normal CSS cascade with no MutationObserver needed (this component paints no canvas/WebGL surface, so the getComputedStyle+MutationObserver token-read rule that applies to raster contexts elsewhere in the registry doesn't apply here — consistent with every other pure-DOM/CSS-var component in this registry, e.g. tag-input-tear, button-cooldown-heat). A11Y: role=separator carries the divider's semantics and needs no accessible name; every rendered element is aria-hidden or purely decorative; there is no keyboard surface because nothing here is operable. Props: height (band height in px, default 44) and className. Zero dependencies, DOM + CSS only.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| height? | number | 44 | band height in px. Flap/needle size derive from this, the strip's own smaller dimension. Default 44. |
| className? | string | — | — |