Section-Trenner
Die Naht zwischen zwei Sections in deren Farben. Wandernde Wellen, Schräge mit Linie, Haarlinie mit Label oder Verlauf.
Die Naht zwischen zwei Sections
Ein Section-Trenner formt die Kante, an der eine Section endet und die nächste beginnt, in den Farben beider. Aus einem harten Farbwechsel wird ein bewusster Übergang, und die bewegten Fassungen folgen sanft dem Scroll.
„Welle“ legt drei Wellen in der Farbe darüber, die beim Scrollen gegeneinander wandern. „Schräge“ setzt eine diagonale Kante zwischen zwei Farben mit feiner Akzentlinie, die sich beim Hereinscrollen zeichnet. „Linie mit Label“ öffnet eine Haarlinie von der Mitte um ein Label, für Kapitel oder ein Oder zwischen zwei Blöcken. „Verlauf“ lässt eine Farbe in die nächste übergehen, während ein weiches Licht mit dem Scroll an der Naht entlangwandert.
Wähl einen Trennerstil pro Website und wiederhole ihn, so wird er Teil der Marke. Wellen passen zu freundlichen Marken, die Schräge zu technischen Produkten. Die Linie mit Label funktioniert auch innerhalb einer Section.
Variants
Details
Was Section-Trenner mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Layouts
- Bereich
- Layout
- Variants
- 4
- Version
- 1.0.0 · seit 1.2.0
- Datei
- components/ui/section-divider.tsx
- Bringt mit
- utilskiticonmotion
- npm-Pakete
- Keine
- Sprachen
- Texte kommen über Props
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle SectionDividerProps aus components/ui/section-divider.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalSectionDividerVariantwaveslantlabelgradient
fromoptionalDividerSurfacebackgroundsurfacemutedcardprimaryinverse
Color of the section above
tooptionalDividerSurfacebackgroundsurfacemutedcardprimaryinverse
Color of the section below
sizeoptional'sm' | 'md' | 'lg'Height of the seam
flipoptionalbooleanwave and slant: mirror left and right
labeloptionalstringlabel: the text in the middle
iconoptionalIconNamelabel: an icon before the text
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle separator.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 6 Stellen im Code schalten Bewegung dann ab.
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 153 Zeilen
'use client';
import { cn, useInView } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
import { useScrollProgress } from '@/components/ui/motion';
/*
* The seam between two sections. It takes the color of the section above
* (`from`) and the one below (`to`), so it fits between any two.
*
* wave layered waves that drift a little against each other while
* the page scrolls
* slant a diagonal edge with a fine accent line that draws itself
* label a hairline with a label in the middle, the line opens from
* the center, for chapters and an "or" between two blocks
* gradient one color fades into the next, a light runs along the seam
*
* Purely decorative shapes are hidden from screen readers, the label is
* read as a separator with its text.
*/
export type SectionDividerVariant = 'wave' | 'slant' | 'label' | 'gradient';
export type DividerSurface = 'background' | 'surface' | 'muted' | 'card' | 'primary' | 'inverse';
export interface SectionDividerProps {
variant?: SectionDividerVariant;
/** Color of the section above */
from?: DividerSurface;
/** Color of the section below */
to?: DividerSurface;
/** Height of the seam */
size?: 'sm' | 'md' | 'lg';
/** wave and slant: mirror left and right */
flip?: boolean;
/** label: the text in the middle */
label?: string;
/** label: an icon before the text */
icon?: IconName;
className?: string;
}
const COLOR: Record<DividerSurface, string> = {
background: 'var(--background)',
surface: 'var(--surface)',
muted: 'var(--muted)',
card: 'var(--card)',
primary: 'var(--primary)',
inverse: 'var(--foreground)',
};
const HEIGHT = { sm: 'h-12 sm:h-16', md: 'h-16 sm:h-24 lg:h-28', lg: 'h-24 sm:h-32 lg:h-40' } as const;
/* ------------------------------------------------------------------ */
function Wave({ from, to, size, flip }: Required<Pick<SectionDividerProps, 'from' | 'to' | 'size'>> & { flip?: boolean }) {
const ref = useScrollProgress<HTMLDivElement>({ reach: false });
// two periods wide, so a layer can slide by one period without an edge showing
const wave = (amp: number, base: number, phase: number) => {
const y = (x: number) => Math.round(base + amp * Math.sin(((x + phase) / 720) * Math.PI * 2));
let d = `M0 0H2880V${y(2880)}`;
for (let x = 2856; x >= 0; x -= 24) d += `L${x} ${y(x)}`;
return d + 'Z';
};
const c = COLOR[from];
return (
<div ref={ref} aria-hidden data-clip-ok className={cn('relative w-full overflow-hidden', HEIGHT[size])} style={{ background: COLOR[to] }}>
<svg viewBox="0 0 1440 120" preserveAspectRatio="none" className={cn('absolute inset-0 size-full', flip && '-scale-x-100')}>
<g style={{ transform: 'translateX(calc((var(--kit-progress, 0.5) - 0.5) * -360px - 360px))' }} className="motion-reduce:!transform-none">
<path d={wave(14, 74, 120)} style={{ fill: c, opacity: 0.35 }} />
</g>
<g style={{ transform: 'translateX(calc((var(--kit-progress, 0.5) - 0.5) * 240px - 360px))' }} className="motion-reduce:!transform-none">
<path d={wave(18, 58, 0)} style={{ fill: c, opacity: 0.6 }} />
</g>
<path d={wave(20, 46, 260)} style={{ fill: c }} />
</svg>
</div>
);
}
function Slant({ from, to, size, flip }: Required<Pick<SectionDividerProps, 'from' | 'to' | 'size'>> & { flip?: boolean }) {
const ref = useScrollProgress<HTMLDivElement>({ start: 0.05, end: 0.6 });
return (
<div ref={ref} aria-hidden data-clip-ok className={cn('relative w-full overflow-hidden', HEIGHT[size])} style={{ background: COLOR[to] }}>
<svg viewBox="0 0 1440 100" preserveAspectRatio="none" className={cn('absolute inset-0 size-full', flip && '-scale-x-100')}>
<polygon points="0,0 1440,0 1440,4 0,100" style={{ fill: COLOR[from] }} />
<line
x1="0"
y1="100"
x2="1440"
y2="4"
pathLength={1}
vectorEffect="non-scaling-stroke"
strokeWidth="2"
strokeLinecap="round"
className="stroke-primary"
style={{ strokeDasharray: 1, strokeDashoffset: 'calc(1 - var(--kit-progress, 1))' }}
/>
</svg>
</div>
);
}
function Label({ from, label, icon }: Pick<SectionDividerProps, 'from' | 'label' | 'icon'>) {
const [ref, inView] = useInView<HTMLDivElement>('0px 0px -15% 0px');
const line = cn('h-px flex-1 transition-transform duration-1000 ease-[var(--ease-kit)] motion-reduce:transition-none', !inView && 'scale-x-0');
return (
<div ref={ref} role="separator" aria-label={label} className="w-full py-10 sm:py-14" style={{ background: COLOR[from ?? 'background'] }}>
<div className="kit-container flex items-center gap-4 sm:gap-6">
<span className={cn(line, 'origin-right bg-[linear-gradient(to_right,transparent,var(--border-strong)_45%)]')} />
{label && (
<span className={cn('inline-flex min-h-9 shrink-0 items-center gap-2 rounded-full border border-border bg-background px-4 text-[13px] font-medium text-foreground shadow-[var(--shadow-sm)] transition-[opacity,transform] delay-300 duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none', !inView && 'translate-y-1 opacity-0')}>
{icon && <Icon name={icon} size={14} className="text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]" />}
{label}
</span>
)}
<span className={cn(line, 'origin-left bg-[linear-gradient(to_left,transparent,var(--border-strong)_45%)]')} />
</div>
</div>
);
}
function Gradient({ from, to, size }: Required<Pick<SectionDividerProps, 'from' | 'to' | 'size'>>) {
const ref = useScrollProgress<HTMLDivElement>({ start: 0.1, end: 0.9 });
return (
<div ref={ref} aria-hidden data-clip-ok className={cn('relative w-full overflow-hidden', HEIGHT[size])} style={{ background: `linear-gradient(to bottom, ${COLOR[from]}, ${COLOR[to]})` }}>
<div className="absolute inset-x-0 top-1/2 h-px bg-[linear-gradient(to_right,transparent,color-mix(in_oklab,var(--primary)_35%,transparent),transparent)]" />
{/* a light that travels along the seam with the scroll */}
<div className="absolute top-1/2 h-px w-[40%] -translate-y-px motion-reduce:hidden" style={{ left: 'calc(var(--kit-progress, 0.5) * 80% - 10%)', background: 'linear-gradient(to right, transparent, var(--primary), transparent)' }} />
<div className="absolute top-1/2 h-16 w-[30%] -translate-y-1/2 rounded-full opacity-60 blur-2xl motion-reduce:hidden" style={{ left: 'calc(var(--kit-progress, 0.5) * 80% - 5%)', background: 'radial-gradient(closest-side, var(--glow), transparent)' }} />
</div>
);
}
/**
* The seam between two sections: waves, a slant with a drawing line, a
* labeled hairline or a gradient with a travelling light.
* <SectionDivider variant="wave" from="surface" to="background" />
*/
export function SectionDivider({ variant = 'wave', from = 'surface', to = 'background', size = 'md', flip, label, icon, className }: SectionDividerProps) {
return (
<div className={cn('relative', className)}>
{variant === 'slant' ? (
<Slant from={from} to={to} size={size} flip={flip} />
) : variant === 'label' ? (
<Label from={from} label={label} icon={icon} />
) : variant === 'gradient' ? (
<Gradient from={from} to={to} size={size} />
) : (
<Wave from={from} to={to} size={size} flip={flip} />
)}
</div>
);
}Häufige Fragen
Wie installiere ich Section-Trenner?
Mit aiw add @ai-website/section-divider. Der Befehl legt die Datei components/ui/section-divider.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon und motion. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Section-Trenner?
4 Varianten, Welle, Schräge, Linie mit Label und Verlauf. Du wählst sie mit der Prop variant, zum Beispiel variant="slant".
Kann ein KI-Assistent Section-Trenner einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Section-Trenner mit aiw_components und installieren sie mit aiw_add @ai-website/section-divider. Nenn im Prompt die Variante, etwa variant="wave", dann schreibt der Assistent deine Inhalte direkt in die Props.