Zum Inhalt springen
UI Kit
Accent

Hinweiskasten für Texte

Hinweise im Text in vier Tönen, Information, Tipp, Achtung und Erledigt. Als ruhige Fläche, Seitenlinie, Ausklapper oder große Fläche.

4 VariantsEine von 5 Components in Artikel-Bausteine

Hinweise, die im richtigen Moment auffallen

Ein Hinweiskasten hebt eine Notiz aus dem Fließtext, einen Tipp, eine Warnung oder die Bestätigung, dass etwas erledigt ist. Alle vier Fassungen gibt es in vier Tönen, Information, Tipp, Achtung und Erledigt, jeder mit eigenem Icon und eigener Farbe, die Bedeutung liest sich also auf einen Blick.

Die „Ruhige Fläche“ ist ein getöntes Feld mit Icon, Titel, Text und Link in der Farbe ihres Tons. Die „Seitenlinie“ ist eine ruhige Linie in der Tonfarbe mit Label, für Texte mit mehreren Hinweisen. „Zum Ausklappen“ öffnet einen Titel zu den Details, für Randbemerkungen, die den Text sonst unterbrechen würden. „Groß mit Schritten“ ist eine große Fläche mit Icon-Kachel und nummerierten Schritten, für den einen Hinweis, um den es auf der Seite geht.

Setz Warnungen sparsam, dann behalten sie ihr Gewicht. Schreib den Titel als Kernaussage, „Erst sichern“ sagt mehr als „Hinweis“. Die große Fläche passt einmal pro Seite, Seitenlinien dürfen sich wiederholen.

Variants

Ruhige FlächeNeu in 1.2

Eine getönte Fläche mit Icon, Titel, Text und Link, in der Farbe ihres Tons.

SeitenlinieNeu in 1.2

Eine ruhige Linie in der Farbe des Tons mit Kennzeichnung, für Texte mit mehreren Hinweisen.

Zum AusklappenNeu in 1.2

Ein Titel, der sich zu den Details öffnet, für Exkurse, die den Text sonst unterbrechen.

Details

Was Hinweiskasten mitbringt, aus Registry und Quellcode gelesen.

Bereich
Content
Variants
4
Version
1.0.0 · seit 1.2.0
Datei
components/ui/callout.tsx
Bringt mit
utilskiticonsection
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 6 eigene Texte
Studio
Im Code anpassen
In Sammlungen
Blog oder Magazin

Props

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

  • variantoptional
    CalloutVariant

    softaccent-barcollapsiblefeatured

  • toneoptional
    CalloutTone

    infotipwarningsuccess

  • titlePflicht
    string
  • textoptional
    string | string[]

    One or more paragraphs

  • pointsoptional
    string[]

    featured: numbered steps, soft and accent-bar: a short list

  • actionoptional
    { label: string; href: string }
  • iconoptional
    IconName

    Overrides the icon of the tone

  • defaultOpenoptional
    boolean

    collapsible: open from the start

  • contextoptional
    { before?: string[]; after?: string[] }

    Paragraphs before and after the note, to show it inside a text

Eingebaute Texte

Diese Texte schreibt Hinweiskasten selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 6 Texte anzeigen
infoEnglisch InformationDeutsch Information
tipEnglisch TipDeutsch Tipp
warningEnglisch WarningDeutsch Achtung
successEnglisch DoneDeutsch Erledigt
moreEnglisch Show detailsDeutsch Details zeigen
lessEnglisch Hide detailsDeutsch Details ausblenden

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Setzt aria-controls, aria-expanded und aria-label, damit Screenreader Zweck und Zustand ansagen.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 3 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, 283 Zeilen
components/ui/callout.tsx
'use client';

import { useId, useState } from 'react';
import { cn, useInView, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
import { Section } from '@/components/ui/section';

/*
 * Notes inside a text that the eye finds while skimming: information, a tip,
 * a warning or a success, each with its own icon and color.
 *
 *   soft         a tinted panel with icon, title, text and a link
 *   accent-bar   a calm line on the side, for many notes in one text
 *   collapsible  a title that opens into the details, for asides
 *   featured     a large panel with an icon tile and numbered steps
 *
 * `context` adds paragraphs before and after, so the note sits in a text.
 * Without it the note stands alone, ready for your own article.
 */

export type CalloutVariant = 'soft' | 'accent-bar' | 'collapsible' | 'featured';
export type CalloutTone = 'info' | 'tip' | 'warning' | 'success';

const TEXTS = {
  en: { info: 'Information', tip: 'Tip', warning: 'Warning', success: 'Done', more: 'Show details', less: 'Hide details' },
  de: { info: 'Information', tip: 'Tipp', warning: 'Achtung', success: 'Erledigt', more: 'Details zeigen', less: 'Details ausblenden' },
};
type Texts = (typeof TEXTS)['en'];

export interface CalloutProps {
  variant?: CalloutVariant;
  tone?: CalloutTone;
  title: string;
  /** One or more paragraphs */
  text?: string | string[];
  /** featured: numbered steps, soft and accent-bar: a short list */
  points?: string[];
  action?: { label: string; href: string };
  /** Overrides the icon of the tone */
  icon?: IconName;
  /** collapsible: open from the start */
  defaultOpen?: boolean;
  /** Paragraphs before and after the note, to show it inside a text */
  context?: { before?: string[]; after?: string[] };
  labels?: Partial<Texts>;
  className?: string;
}

/* Tones: an icon, a text color readable in both modes and a tinted surface */
const TONE: Record<CalloutTone, { icon: IconName; text: string; ink: string; surface: string; line: string; tile: string }> = {
  info: {
    icon: 'info',
    text: 'text-info',
    ink: 'text-[color-mix(in_oklab,var(--info)_80%,var(--foreground))]',
    surface: 'bg-[color-mix(in_oklab,var(--info)_9%,var(--card))]',
    line: 'border-[color-mix(in_oklab,var(--info)_30%,var(--border))]',
    tile: 'bg-[color-mix(in_oklab,var(--info)_16%,var(--card))]',
  },
  tip: {
    icon: 'lightbulb',
    text: 'text-primary',
    ink: 'text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]',
    surface: 'bg-[color-mix(in_oklab,var(--primary)_8%,var(--card))]',
    line: 'border-[color-mix(in_oklab,var(--primary)_30%,var(--border))]',
    tile: 'bg-[color-mix(in_oklab,var(--primary)_15%,var(--card))]',
  },
  warning: {
    icon: 'alert',
    text: 'text-warning-foreground',
    ink: 'text-warning-foreground',
    surface: 'bg-[color-mix(in_oklab,var(--warning)_13%,var(--card))]',
    line: 'border-[color-mix(in_oklab,var(--warning)_45%,var(--border))]',
    tile: 'bg-[color-mix(in_oklab,var(--warning)_22%,var(--card))]',
  },
  success: {
    icon: 'check-circle',
    text: 'text-success-foreground',
    ink: 'text-success-foreground',
    surface: 'bg-[color-mix(in_oklab,var(--success)_10%,var(--card))]',
    line: 'border-[color-mix(in_oklab,var(--success)_35%,var(--border))]',
    tile: 'bg-[color-mix(in_oklab,var(--success)_17%,var(--card))]',
  },
};
const BAR: Record<CalloutTone, string> = { info: 'bg-info', tip: 'bg-primary', warning: 'bg-warning', success: 'bg-success' };

const paras = (text?: string | string[]) => (text === undefined ? [] : Array.isArray(text) ? text : [text]);

function Link({ action, className }: { action?: CalloutProps['action']; className?: string }) {
  if (!action) return null;
  return (
    <a
      href={action.href}
      className={cn(
        'group/link inline-flex min-h-11 items-center gap-1.5 text-[14px] font-medium text-foreground underline decoration-border-strong underline-offset-4 transition-colors hover:decoration-foreground outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-ring',
        className,
      )}
    >
      {action.label}
      <Icon name="arrow-right" size={14} className="transition-transform duration-300 group-hover/link:translate-x-0.5" />
    </a>
  );
}

function Points({ points, tone }: { points?: string[]; tone: CalloutTone }) {
  if (!points?.length) return null;
  return (
    <ul className="mt-3 space-y-1.5">
      {points.map((pt) => (
        <li key={pt} className="flex gap-2.5 text-[15px] leading-relaxed text-muted-foreground">
          <span aria-hidden className={cn('mt-[0.7em] size-1.5 shrink-0 rounded-full', BAR[tone])} />
          <span>{pt}</span>
        </li>
      ))}
    </ul>
  );
}

/** The note rises in once and its icon gives a small turn, nothing more */
function useEntrance() {
  const [ref, inView] = useInView<HTMLElement>('0px 0px -10% 0px');
  return { ref, cls: cn('transition-[opacity,transform] duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none', !inView && 'translate-y-3 opacity-0'), icon: cn('transition-transform delay-200 duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none', !inView && '-rotate-12 scale-75') };
}

/* ------------------------------------------------------------------ */

function Soft(p: CalloutProps & { t: Texts }) {
  const tone = p.tone ?? 'info';
  const k = TONE[tone];
  const e = useEntrance();
  return (
    <aside ref={e.ref} aria-label={p.t[tone]} className={cn('rounded-xl border p-5 sm:p-6', k.surface, k.line, e.cls)}>
      <div className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4">
        <span className={cn('mt-0.5 flex size-8 items-center justify-center rounded-lg', k.tile, k.text)}>
          <Icon name={p.icon ?? k.icon} size={17} className={e.icon} />
        </span>
        <div>
          <p className={cn('pt-[0.3rem] text-[15.5px] font-semibold leading-snug', k.ink)}>{p.title}</p>
          {paras(p.text).map((t, i) => (
            <p key={i} className="mt-1.5 text-pretty text-[15px] leading-relaxed text-[color-mix(in_oklab,var(--foreground)_78%,var(--background))]">
              {t}
            </p>
          ))}
          <Points points={p.points} tone={tone} />
          <Link action={p.action} className="-mb-2 mt-2" />
        </div>
      </div>
    </aside>
  );
}

function AccentBar(p: CalloutProps & { t: Texts }) {
  const tone = p.tone ?? 'info';
  const k = TONE[tone];
  const e = useEntrance();
  return (
    <aside ref={e.ref} aria-label={p.t[tone]} className={cn('relative py-1 pl-6', e.cls)}>
      <span aria-hidden className={cn('absolute inset-y-0 left-0 w-[3px] rounded-full', BAR[tone])} />
      <p className={cn('flex items-center gap-2 text-[12px] font-semibold uppercase tracking-[0.08em]', k.ink)}>
        <Icon name={p.icon ?? k.icon} size={15} className={e.icon} />
        {p.t[tone]}
      </p>
      <p className="mt-2 text-[16px] font-semibold leading-snug text-foreground">{p.title}</p>
      {paras(p.text).map((t, i) => (
        <p key={i} className="mt-1.5 text-pretty text-[15.5px] leading-relaxed text-muted-foreground">
          {t}
        </p>
      ))}
      <Points points={p.points} tone={tone} />
      <Link action={p.action} className="-mb-2 mt-1" />
    </aside>
  );
}

function Collapsible(p: CalloutProps & { t: Texts }) {
  const tone = p.tone ?? 'info';
  const k = TONE[tone];
  const [open, setOpen] = useState(!!p.defaultOpen);
  const id = useId();
  const e = useEntrance();
  return (
    <aside ref={e.ref} aria-label={p.t[tone]} className={cn('overflow-hidden rounded-xl border bg-card shadow-[var(--highlight),var(--shadow-sm)]', k.line, e.cls)}>
      <button
        type="button"
        aria-expanded={open}
        aria-controls={id}
        onClick={() => setOpen((o) => !o)}
        className={cn('group flex min-h-14 w-full cursor-pointer items-center gap-4 px-5 py-3 text-left transition-colors outline-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-ring', open ? k.surface : 'hover:bg-surface')}
      >
        <span className={cn('flex size-8 shrink-0 items-center justify-center rounded-lg', k.tile, k.text)}>
          <Icon name={p.icon ?? k.icon} size={17} className={e.icon} />
        </span>
        <span className="min-w-0 flex-1">
          <span className={cn('block text-[12px] font-medium', k.ink)}>{p.t[tone]}</span>
          <span className="block text-[15px] font-semibold leading-snug text-foreground">{p.title}</span>
        </span>
        <span className="hidden text-[13px] text-muted-foreground sm:block">{open ? p.t.less : p.t.more}</span>
        <span className={cn('flex size-8 shrink-0 items-center justify-center rounded-full border border-border bg-background transition-transform duration-300 ease-[var(--ease-kit)]', open && 'rotate-45')}>
          <Icon name="plus" size={15} />
        </span>
      </button>
      <div id={id} className={cn('grid transition-[grid-template-rows] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]')}>
        <div className={cn('min-h-0 overflow-hidden', !open && 'invisible')}>
          <div className={cn('border-t px-5 pb-5 pt-4 sm:pl-[4.25rem]', k.line)}>
            {paras(p.text).map((t, i) => (
              <p key={i} className="mt-2 text-pretty text-[15px] leading-relaxed text-muted-foreground first:mt-0">
                {t}
              </p>
            ))}
            <Points points={p.points} tone={tone} />
            <Link action={p.action} className="-mb-2 mt-2" />
          </div>
        </div>
      </div>
    </aside>
  );
}

function Featured(p: CalloutProps & { t: Texts }) {
  const tone = p.tone ?? 'tip';
  const k = TONE[tone];
  const e = useEntrance();
  return (
    <aside ref={e.ref} aria-label={p.t[tone]} className={cn('relative overflow-hidden rounded-2xl border p-6 shadow-[var(--highlight),var(--shadow-md)] sm:p-8', k.surface, k.line, e.cls)}>
      <div aria-hidden data-clip-ok className={cn('pointer-events-none absolute -right-16 -top-16 size-56 rounded-full opacity-60 blur-3xl', k.tile)} />
      <div className="relative grid grid-cols-[minmax(0,1fr)] gap-6 sm:grid-cols-[auto_minmax(0,1fr)] sm:gap-7">
        <span className={cn('flex size-14 items-center justify-center rounded-2xl border bg-card shadow-[var(--shadow-sm)]', k.line, k.text)}>
          <Icon name={p.icon ?? k.icon} size={26} strokeWidth={1.5} className={e.icon} />
        </span>
        <div>
          <p className={cn('text-[13px] font-medium', k.ink)}>{p.t[tone]}</p>
          <p className="mt-1 text-balance text-[clamp(1.25rem,1.1rem+0.5vw,1.5rem)] font-semibold leading-tight tracking-[-0.02em] text-foreground">{p.title}</p>
          {paras(p.text).map((t, i) => (
            <p key={i} className="mt-3 max-w-[38rem] text-pretty text-[15.5px] leading-relaxed text-[color-mix(in_oklab,var(--foreground)_78%,var(--background))]">
              {t}
            </p>
          ))}
          {p.points?.length ? (
            <ol className="mt-6 grid grid-cols-[minmax(0,1fr)] gap-3 sm:grid-cols-2">
              {p.points.map((pt, i) => (
                <li key={pt} className="flex gap-3 rounded-xl border border-border bg-card/80 p-4 text-[14.5px] leading-snug text-foreground">
                  <span className={cn('flex size-6 shrink-0 items-center justify-center rounded-full text-[12px] font-semibold tabular-nums', k.tile, k.ink)}>{i + 1}</span>
                  <span className="pt-0.5">{pt}</span>
                </li>
              ))}
            </ol>
          ) : null}
          <Link action={p.action} className="-mb-2 mt-4" />
        </div>
      </div>
    </aside>
  );
}

const PROSE = 'text-pretty text-[17px] leading-[1.75] text-[color-mix(in_oklab,var(--foreground)_86%,var(--background))]';

/**
 * A note in a text: information, a tip, a warning or a success, as a soft
 * panel, a side line, a note that opens or a large panel with steps.
 *   <Callout variant="soft" tone="warning" title="…" text="…" />
 */
export function Callout(props: CalloutProps) {
  const { variant = 'soft', className, labels, context } = props;
  const t = useTexts(TEXTS, labels);
  const note = variant === 'accent-bar' ? <AccentBar {...props} t={t} /> : variant === 'collapsible' ? <Collapsible {...props} t={t} /> : variant === 'featured' ? <Featured {...props} t={t} /> : <Soft {...props} t={t} />;
  if (!context) return <div className={className}>{note}</div>;
  return (
    <Section spacing="sm" className={className}>
      <div className={cn('mx-auto', variant === 'featured' ? 'max-w-[48rem]' : 'max-w-[38rem]')}>
        {context.before?.map((b, i) => (
          <p key={i} className={cn(PROSE, 'mb-6', variant === 'featured' && 'mx-auto max-w-[38rem]')}>
            {b}
          </p>
        ))}
        <div className="my-9">{note}</div>
        {context.after?.map((b, i) => (
          <p key={i} className={cn(PROSE, 'mt-6', variant === 'featured' && 'mx-auto max-w-[38rem]')}>
            {b}
          </p>
        ))}
      </div>
    </Section>
  );
}

Häufige Fragen

Wie installiere ich Hinweiskasten?

Mit aiw add @ai-website/callout. Der Befehl legt die Datei components/ui/callout.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon und section. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Hinweiskasten?

4 Varianten, Ruhige Fläche, Seitenlinie, Zum Ausklappen und Groß mit Schritten. Du wählst sie mit der Prop variant, zum Beispiel variant="accent-bar".

Kann ich die Texte von Hinweiskasten übersetzen?

Ja. Hinweiskasten bringt 6 eigene Texte auf Deutsch und Englisch mit, etwa „Information“, „Tipp“ und „Achtung“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Hinweiskasten einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Hinweiskasten mit aiw_components und installieren sie mit aiw_add @ai-website/callout. Nenn im Prompt die Variante, etwa variant="soft", dann schreibt der Assistent deine Inhalte direkt in die Props.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert