Zum Inhalt springen
UI Kit
Accent

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.

5 VariantsEine von 9 Components in Data Display

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

Mit BeschriftungNeu in 1.2

Die Beschriftung sitzt mittig, am Anfang oder am Ende: zwischen zwei Wegen der Anmeldung, als Datum im Chat, als Kopf eines ruhigen Bereichs in Einstellungen.

Symbol und OrnamentNeu in 1.2

Ein Szenenwechsel im langen Text, ein Zeichen über einem Titel, eine Welle oder eine Raute als Abschluss einer Liste. Wo es leicht wirken soll, läuft die Linie in die Fläche aus.

Mit AktionNeu in 1.2

Die Linie trägt einen Knopf: frühere Antworten über einem Verlauf, mehr Aktivität am Ende eines Feeds. Drück sie, die neuen Zeilen steigen ein und die Linie sagt, wenn alles da ist.

AufrechtNeu in 1.2

Aufrecht in einer Werkzeugleiste und zwischen Metadaten, zwischen Kennzahlen, die am Telefon untereinander stehen, und zwischen zwei Spalten, wo dieselbe Linie unter 1024 px liegt.

Eingerückt und gestricheltNeu in 1.2

In einer Liste beginnt die Linie dort, wo der Text beginnt, nach dem Avatar. Auf einem Beleg halten gestrichelte und gepunktete Linien den Innenabstand und lassen die Ränder frei.

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

Props

Die Schnittstelle DividerProps aus components/ui/divider.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • orientationoptional
    DividerOrientation

    horizontalverticalresponsive

    responsive: lies flat on phones and tablets, stands upright from 1024 px on (two columns side by side)

  • labeloptional
    React.ReactNode

    Text 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

  • iconoptional
    IconName
  • ornamentoptional
    DividerOrnament

    dotsdiamondasterismwave

  • actionoptional
    DividerAction

    A button or link in the line, e.g. "Load more"

  • lineoptional
    DividerLine

    soliddasheddotted

  • toneoptional
    DividerTone

    defaultstrongaccentfade

  • insetoptional
    'none' | 'start' | 'both' | number

    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.

  • 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
components/ui/divider.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert