Trennlinie
Linien zwischen Inhalten innerhalb einer Sektion, mit Beschriftung, Symbol oder Ornament, mit Aktion wie Mehr laden, aufrecht in Leisten, eingerückt in Listen, gestrichelt oder gepunktet.
Linien, die gliedern, ohne laut zu werden
Eine Trennlinie gliedert Inhalt innerhalb einer Section, zwischen zwei Wegen der Anmeldung, zwischen Tagen im Chat oder zwischen Kennzahlen in einer Reihe. Jede Linie hier sitzt auf ganzen Pixeln und bleibt bei 1 px scharf, den Raum zwischen ganzen Sections übernehmen die Section-Übergänge.
„Mit Beschriftung“ setzt den Text mittig, an den Anfang oder ans Ende, zwischen zwei Wege der Anmeldung, als Datum im Chat oder als Kopf eines ruhigen Bereichs in Einstellungen. „Symbol und Ornament“ markiert einen Szenenwechsel im langen Text, ein Zeichen über einem Titel oder eine Welle oder Raute als Abschluss einer Liste. „Mit Aktion“ lässt die Linie einen Knopf tragen, frühere Antworten über einem Verlauf oder mehr Aktivität am Ende eines Feeds. „Aufrecht“ steht in Werkzeugleisten, zwischen Metadaten und zwischen Kennzahlen und liegt unter 1024 px flach. „Eingerückt und gestrichelt“ beginnt die Linie in Listen dort, wo der Text beginnt, und zeichnet gestrichelte und gepunktete Linien auf einem Beleg.
Gliedere zuerst mit Abstand und nimm eine Linie nur, wo Abstand nicht reicht. Eine beschriftete Linie erklärt eine Wahl, oder mit E-Mail anmelden, besser als eine leere. Eingerückte Linien in Listen lassen Avatare frei und machen Zeilen leichter lesbar.
Variants
Details
Was Trennlinie mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Data Display
- Bereich
- Elements
- Variants
- 5
- Version
- 1.0.0 · seit 1.2.0
- Datei
- components/ui/divider.tsx
- Bringt mit
- utilskiticon
- 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 DividerProps aus components/ui/divider.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
orientationoptionalDividerOrientationhorizontalverticalresponsive
responsive: lies flat on phones and tablets, stands upright from 1024 px on (two columns side by side)
labeloptionalReact.ReactNodeText on the line, e.g. "or" or a date. Read out as text, the line itself stays silent.
alignoptional'start' | 'center' | 'end'Where the label, icon or action sits on the line
iconoptionalIconNameornamentoptionalDividerOrnamentdotsdiamondasterismwave
actionoptionalDividerActionA button or link in the line, e.g. "Load more"
lineoptionalDividerLinesoliddasheddotted
toneoptionalDividerTonedefaultstrongaccentfade
insetoptional'none' | 'start' | 'both' | numberIndent so the line starts where the text starts. start: the width of a list avatar or icon (3.5rem, change it with --divider-inset). both: inset on both sides. A number is the start indent in px.
spacingoptional'none' | 'sm' | 'md' | 'lg'Space around the divider along its axis
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-orientation, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rollen label und separator.
- Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
- 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. 1 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, 237 Zeilen
'use client';
import { cn } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
/*
* Divider: the line between two things inside a section, a card, a list or a
* toolbar. A plain hairline, a line with a label ("or", a date), with an icon
* or an ornament, with an action in the middle ("Show 12 earlier replies"),
* standing upright in a bar, or indented so it starts where the text starts.
* Page transitions between whole sections are a different thing (section
* transitions), this one stays inside the content.
*
* Lines are 1 px borders, never scaled backgrounds, so they land on whole
* pixels and meet the edges of the surface they sit on.
*/
export type DividerOrientation = 'horizontal' | 'vertical' | 'responsive';
export type DividerLine = 'solid' | 'dashed' | 'dotted';
export type DividerTone = 'default' | 'strong' | 'accent' | 'fade';
export type DividerOrnament = 'dots' | 'diamond' | 'asterism' | 'wave';
export interface DividerAction {
label: string;
/** Turns the action into a link */
href?: string;
onClick?: () => void;
icon?: IconName;
/** Quiet count after the label, e.g. hidden replies */
count?: number;
disabled?: boolean;
}
export interface DividerProps {
/** responsive: lies flat on phones and tablets, stands upright from 1024 px on (two columns side by side) */
orientation?: DividerOrientation;
/** Text on the line, e.g. "or" or a date. Read out as text, the line itself stays silent. */
label?: React.ReactNode;
/** Where the label, icon or action sits on the line */
align?: 'start' | 'center' | 'end';
icon?: IconName;
ornament?: DividerOrnament;
/** A button or link in the line, e.g. "Load more" */
action?: DividerAction;
line?: DividerLine;
tone?: DividerTone;
/**
* Indent so the line starts where the text starts. start: the width of a list
* avatar or icon (3.5rem, change it with --divider-inset). both: inset on both sides.
* A number is the start indent in px.
*/
inset?: 'none' | 'start' | 'both' | number;
/** Space around the divider along its axis */
spacing?: 'none' | 'sm' | 'md' | 'lg';
className?: string;
}
const BORDER_STYLE: Record<DividerLine, string> = { solid: 'border-solid', dashed: 'border-dashed', dotted: 'border-dotted' };
const BORDER_TONE: Record<Exclude<DividerTone, 'fade'>, string> = {
default: 'border-border',
strong: 'border-border-strong',
accent: 'border-[color-mix(in_oklab,var(--primary)_55%,var(--border))]',
};
/** A fading line needs more ink than a border to read, it is thin only at its ends */
const FADE = 'color-mix(in oklab, var(--foreground) 30%, transparent)';
const SPACE_Y = { none: '', sm: 'my-3', md: 'my-6', lg: 'my-10' } as const;
const SPACE_X = { none: '', sm: 'mx-2', md: 'mx-4', lg: 'mx-6' } as const;
/** One stretch of line. `side` says which end fades out with the fade tone. */
/*
* One stretch of line beside a label, icon or action. The line sits in a box as tall as the row
* and is placed at the middle rounded down to a whole pixel, so it never lands on a half pixel
* and blurs, whatever height the label has.
*/
function Line({ vertical, line, tone, fadeTo, className }: { vertical: boolean | 'responsive'; line: DividerLine; tone: DividerTone; fadeTo?: 'start' | 'end' | 'both'; className?: string }) {
const H = 'inset-x-0 top-[round(down,50%,1px)]';
const V = 'inset-y-0 left-[round(down,50%,1px)]';
const R = 'lg:inset-x-auto lg:inset-y-0 lg:top-0 lg:left-[round(down,50%,1px)]';
let inner: React.ReactNode;
if (tone === 'fade') {
// A fading line cannot be a border, it is a 1 px gradient that ends in the surface
const dir = fadeTo === 'start' ? 'to left' : fadeTo === 'end' ? 'to right' : null;
const grad = dir ? `linear-gradient(${dir}, var(--divider-fade), transparent)` : 'linear-gradient(to right, transparent, var(--divider-fade) 20%, var(--divider-fade) 80%, transparent)';
const gradV = dir ? `linear-gradient(${fadeTo === 'start' ? 'to top' : 'to bottom'}, var(--divider-fade), transparent)` : 'linear-gradient(to bottom, transparent, var(--divider-fade) 20%, var(--divider-fade) 80%, transparent)';
inner = (
<span
className={cn(
'absolute block',
vertical === true ? cn(V, 'w-px bg-[image:var(--gv)]') : vertical === 'responsive' ? cn(H, R, 'h-px bg-[image:var(--g)] lg:h-auto lg:w-px lg:bg-[image:var(--gv)]') : cn(H, 'h-px bg-[image:var(--g)]'),
)}
style={{ '--g': grad, '--gv': gradV, '--divider-fade': FADE } as React.CSSProperties}
/>
);
} else {
inner = (
<span
className={cn(
'absolute block',
vertical === true ? cn(V, 'w-0 border-l') : vertical === 'responsive' ? cn(H, R, 'h-0 border-t lg:h-auto lg:w-0 lg:border-l lg:border-t-0') : cn(H, 'h-0 border-t'),
BORDER_STYLE[line],
BORDER_TONE[tone],
)}
/>
);
}
return (
<span aria-hidden className={cn('relative block flex-1 self-stretch', vertical === true ? 'min-h-3' : vertical === 'responsive' ? 'min-w-3 lg:min-h-3 lg:min-w-0' : 'min-w-3', className)}>
{inner}
</span>
);
}
const ORNAMENT: Record<DividerOrnament, React.ReactNode> = {
dots: (
<svg width="28" height="6" viewBox="0 0 28 6" aria-hidden className="block">
<circle cx="3" cy="3" r="2" fill="currentColor" />
<circle cx="14" cy="3" r="2" fill="currentColor" />
<circle cx="25" cy="3" r="2" fill="currentColor" />
</svg>
),
diamond: (
<svg width="22" height="10" viewBox="0 0 22 10" aria-hidden className="block">
<path d="M11 1 15 5 11 9 7 5Z" fill="currentColor" />
<path d="M2 5h3M17 5h3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
</svg>
),
asterism: (
<svg width="26" height="20" viewBox="0 0 26 20" aria-hidden className="block">
{[
[13, 5],
[6, 15],
[20, 15],
].map(([x, y]) => (
<path key={`${x}${y}`} d={`M${x} ${y - 4}v8M${x - 3.5} ${y - 2}l7 4M${x - 3.5} ${y + 2}l7-4`} stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
))}
</svg>
),
wave: (
<svg width="44" height="10" viewBox="0 0 44 10" aria-hidden className="block">
<path d="M1 5c3.5 0 3.5-4 7-4s3.5 8 7 8 3.5-8 7-8 3.5 8 7 8 3.5-8 7-8 3.5 4 7 4" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
),
};
/**
* A line between two pieces of content, plain or with a label, icon, ornament or action.
* <Divider label="or continue with" />
* <Divider action={{ label: 'Show 12 earlier replies', onClick }} />
* <Divider orientation="vertical" />
*/
export function Divider({ orientation = 'horizontal', label, align = 'center', icon, ornament, action, line = 'solid', tone = 'default', inset = 'none', spacing = 'none', className }: DividerProps) {
const vertical = orientation === 'vertical' ? true : orientation === 'responsive' ? 'responsive' : false;
const insetStyle = typeof inset === 'number' ? ({ marginInlineStart: inset } as React.CSSProperties) : undefined;
const insetClass = inset === 'start' ? 'ms-[var(--divider-inset,3.5rem)]' : inset === 'both' ? 'mx-[var(--divider-inset,1rem)]' : '';
const spaceClass = vertical === true ? SPACE_X[spacing] : vertical === 'responsive' ? cn(SPACE_Y[spacing], spacing !== 'none' && 'lg:my-0', SPACE_X[spacing].replace('mx-', 'lg:mx-')) : SPACE_Y[spacing];
const hasContent = Boolean(label || icon || ornament || action);
// A bare line: one element with the separator role, a border on whole pixels
if (!hasContent) {
return (
<div
role="separator"
aria-orientation={vertical === true ? 'vertical' : 'horizontal'}
className={cn(
'shrink-0',
vertical === true ? 'w-0 self-stretch border-l' : vertical === 'responsive' ? 'h-0 w-full border-t lg:h-auto lg:w-0 lg:self-stretch lg:border-l lg:border-t-0' : 'h-0 w-auto border-t',
tone === 'fade' ? 'border-transparent' : cn(BORDER_STYLE[line], BORDER_TONE[tone]),
tone === 'fade' && (vertical === true ? 'w-px bg-[linear-gradient(to_bottom,transparent,var(--divider-fade)_20%,var(--divider-fade)_80%,transparent)]' : 'h-px bg-[linear-gradient(to_right,transparent,var(--divider-fade)_20%,var(--divider-fade)_80%,transparent)]'),
vertical === true ? 'min-h-4' : '',
insetClass,
spaceClass,
className,
)}
style={{ ...insetStyle, '--divider-fade': FADE } as React.CSSProperties}
/>
);
}
const middle = action ? (
<ActionButton action={action} />
) : (
<span className={cn('flex shrink-0 items-center gap-2', vertical === true ? 'flex-col' : vertical === 'responsive' && 'lg:flex-col')}>
{icon && (
<span className="grid size-8 place-items-center rounded-full border border-border bg-background text-muted-foreground shadow-[var(--shadow-sm)]">
<Icon name={icon} size={15} />
</span>
)}
{ornament && <span className={cn('text-subtle-foreground', tone === 'accent' && 'text-primary')}>{ORNAMENT[ornament]}</span>}
{label && <span className="text-[12px] font-medium leading-5 text-muted-foreground">{label}</span>}
</span>
);
const before = align !== 'start';
const after = align !== 'end';
return (
<div
// Without a label the drawing is decoration, the whole thing is one separator. With text or a button the content is read, the lines stay silent.
role={label || action ? undefined : 'separator'}
aria-orientation={label || action ? undefined : vertical === true ? 'vertical' : 'horizontal'}
className={cn(
'flex shrink-0 items-center',
vertical === true ? 'min-h-16 flex-col self-stretch gap-2.5' : vertical === 'responsive' ? 'w-full gap-3 lg:w-auto lg:flex-col lg:self-stretch lg:gap-2.5' : 'gap-3',
insetClass,
spaceClass,
className,
)}
style={insetStyle}
>
{before ? <Line vertical={vertical} line={line} tone={tone} fadeTo={tone === 'fade' ? 'start' : undefined} /> : null}
{middle}
{after ? <Line vertical={vertical} line={line} tone={tone} fadeTo={tone === 'fade' ? 'end' : undefined} /> : null}
</div>
);
}
function ActionButton({ action }: { action: DividerAction }) {
const cls =
'relative inline-flex h-8 shrink-0 cursor-pointer select-none items-center gap-1.5 rounded-lg border border-border-strong bg-background px-3 text-[13px] font-medium text-foreground shadow-[var(--shadow-sm)] outline-none transition-[background-color,color,scale] duration-200 ease-[var(--ease-kit)] after:absolute after:-inset-x-1 after:-inset-y-1.5 after:content-[""] hover:bg-accent focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-ring active:scale-[0.97] disabled:pointer-events-none disabled:opacity-50 motion-reduce:active:scale-100 max-sm:h-9';
const inner = (
<>
{action.icon && <Icon name={action.icon} size={14} className="text-muted-foreground" />}
{action.label}
{action.count !== undefined && <span className="tabular-nums text-subtle-foreground">{action.count}</span>}
</>
);
if (action.href)
return (
<a href={action.href} onClick={action.onClick} className={cls}>
{inner}
</a>
);
return (
<button type="button" onClick={action.onClick} disabled={action.disabled} className={cls}>
{inner}
</button>
);
}Häufige Fragen
Wie installiere ich Trennlinie?
Mit aiw add @ai-website/divider. Der Befehl legt die Datei components/ui/divider.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Trennlinie?
5 Varianten, Mit Beschriftung, Symbol und Ornament, Mit Aktion, Aufrecht und Eingerückt und gestrichelt.
Kann ein KI-Assistent Trennlinie einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Trennlinie mit aiw_components und installieren sie mit aiw_add @ai-website/divider.