Knopfgruppe
Knöpfe zu einem Bedienelement verbunden, als Reihe, Stapel oder geteilter Knopf mit Menü.
Eine von 6 Components in Buttons
Details
Was Knopfgruppe mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Buttons
- Bereich
- Elements
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/button-group.tsx
- Bringt mit
- utilskitbuttondropdown-menuicon
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 1 eigener Text
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle ButtonGroupProps aus components/ui/button-group.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
orientationoptional'horizontal' | 'vertical'horizontal is the default
sizeoptionalButtonSizeThe size of the buttons inside, sets the outer radius
labeloptionalstringAccessible name of the group
childrenPflichtReact.ReactNode
Eingebaute Texte
Diese Texte schreibt Knopfgruppe selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 1 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| more | Englisch More options | Deutsch Weitere Optionen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle group.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 142 Zeilen
'use client';
import { useRef } from 'react';
import { cn, prefersReducedMotion, useTexts } from '@/lib/kit';
import { Button, type ButtonSize, type ButtonVariant } from '@/components/ui/button';
import { DropdownMenu, type MenuItem } from '@/components/ui/dropdown-menu';
import { Icon } from '@/components/ui/icon';
const TEXTS = { en: { more: 'More options' }, de: { more: 'Weitere Optionen' } };
const RADIUS: Record<ButtonSize, { h: string; v: string }> = {
sm: { h: '[&>*:first-child]:rounded-s-md [&>*:last-child]:rounded-e-md', v: '[&>*:first-child]:rounded-t-md [&>*:last-child]:rounded-b-md' },
md: { h: '[&>*:first-child]:rounded-s-lg [&>*:last-child]:rounded-e-lg', v: '[&>*:first-child]:rounded-t-lg [&>*:last-child]:rounded-b-lg' },
lg: { h: '[&>*:first-child]:rounded-s-lg [&>*:last-child]:rounded-e-lg', v: '[&>*:first-child]:rounded-t-lg [&>*:last-child]:rounded-b-lg' },
};
export interface ButtonGroupProps {
/** horizontal is the default */
orientation?: 'horizontal' | 'vertical';
/** The size of the buttons inside, sets the outer radius */
size?: ButtonSize;
/** Accessible name of the group */
label?: string;
className?: string;
children: React.ReactNode;
}
/**
* Buttons joined into one control. Borders overlap by one pixel, so no line is ever doubled.
* Pass Buttons (or IconButtons, DropdownMenus) as children.
* A thin accent line glides to the button under the mouse or with keyboard focus.
*/
export function ButtonGroup({ orientation = 'horizontal', size = 'md', label, className, children }: ButtonGroupProps) {
const h = orientation === 'horizontal';
const ref = useRef<HTMLDivElement>(null);
// The line is the group's own ::after, placed with CSS variables, so it never counts as a child
const place = (target: EventTarget | null) => {
const root = ref.current;
if (!root || prefersReducedMotion()) return;
const child = Array.from(root.children).find((c) => c.contains(target as Node)) as HTMLElement | undefined;
if (!child) return;
const st = root.style;
if (h) {
st.setProperty('--line-x', `${child.offsetLeft + 10}px`);
st.setProperty('--line-y', `${child.offsetTop + child.offsetHeight - 4}px`);
st.setProperty('--line-w', `${Math.max(8, child.offsetWidth - 20)}px`);
st.setProperty('--line-h', '2px');
} else {
st.setProperty('--line-x', `${child.offsetLeft + 4}px`);
st.setProperty('--line-y', `${child.offsetTop + 10}px`);
st.setProperty('--line-w', '2px');
st.setProperty('--line-h', `${Math.max(8, child.offsetHeight - 20)}px`);
}
if (root.dataset.line) return;
root.dataset.line = 'on';
// First placement without travel, the line only fades in; from then on it glides
requestAnimationFrame(() => requestAnimationFrame(() => ref.current?.dataset.line && (ref.current.dataset.glide = 'on')));
};
const hide = () => {
const root = ref.current;
if (!root || root.contains(document.activeElement)) return;
delete root.dataset.line;
delete root.dataset.glide;
};
return (
<div
ref={ref}
role="group"
aria-label={label}
onPointerOver={(e) => e.pointerType === 'mouse' && place(e.target)}
onPointerLeave={hide}
onFocus={(e) => place(e.target)}
onBlur={(e) => {
if (!ref.current?.contains(e.relatedTarget as Node)) {
const root = ref.current;
if (root) {
delete root.dataset.line;
delete root.dataset.glide;
}
}
}}
className={cn(
'relative isolate inline-flex',
'after:pointer-events-none after:absolute after:left-0 after:top-0 after:z-30 after:h-[var(--line-h,2px)] after:w-[var(--line-w,0px)] after:rounded-full after:bg-primary after:opacity-0 after:content-[""] after:[translate:var(--line-x,0px)_var(--line-y,0px)] after:transition-opacity after:duration-200',
'data-line:after:opacity-100 data-glide:after:transition-[opacity,translate,width,height] data-glide:after:duration-300 data-glide:after:ease-[var(--ease-kit)]',
h ? 'flex-row' : 'flex-col',
'[&>*]:rounded-none [&>*:hover]:z-10 [&>*:focus-visible]:z-20 [&>*]:relative',
'[&>span>*]:rounded-none [&>span:first-child>*]:rounded-[inherit] [&>span:last-child>*]:rounded-[inherit]',
h ? '[&>*:not(:first-child)]:-ms-px' : '[&>*:not(:first-child)]:-mt-px',
h ? RADIUS[size].h : RADIUS[size].v,
className,
)}
>
{children}
</div>
);
}
export interface SplitButtonProps {
label: React.ReactNode;
onClick?: () => void;
href?: string;
/** The other actions, behind the chevron */
items: MenuItem[];
variant?: Extract<ButtonVariant, 'primary' | 'outline' | 'secondary'>;
size?: ButtonSize;
iconLeft?: React.ComponentProps<typeof Button>['iconLeft'];
labels?: Partial<(typeof TEXTS)['en']>;
className?: string;
}
/** The main action plus a menu of related ones, e.g. Publish / Schedule / Save as draft. */
export function SplitButton({ label, onClick, href, items, variant = 'primary', size = 'md', iconLeft, labels, className }: SplitButtonProps) {
const t = useTexts(TEXTS, labels);
const divider =
variant === 'primary'
? 'before:absolute before:inset-y-2.5 before:start-0 before:w-px before:bg-[color-mix(in_oklab,var(--primary-foreground)_24%,transparent)]'
: '';
const chevronWidth = size === 'sm' ? 'w-9 px-0!' : size === 'md' ? 'w-11 px-0!' : 'w-12 px-0!';
return (
<ButtonGroup size={size} className={className}>
{href ? (
<Button href={href} variant={variant} size={size} iconLeft={iconLeft}>
{label}
</Button>
) : (
<Button onClick={onClick} variant={variant} size={size} iconLeft={iconLeft}>
{label}
</Button>
)}
<DropdownMenu
align="end"
items={items}
trigger={
<Button variant={variant} size={size} aria-label={t.more} className={cn('relative rounded-s-none rounded-e-[inherit]', chevronWidth, divider, variant === 'primary' && 'ms-0')}>
<Icon name="chevron-down" size={size === 'sm' ? 15 : 17} className="transition-[rotate] duration-300 ease-[var(--ease-kit)] group-aria-expanded/button:rotate-180 motion-reduce:transition-none" />
</Button>
}
/>
</ButtonGroup>
);
}Häufige Fragen
Wie installiere ich Knopfgruppe?
Mit aiw add @ai-website/button-group. Der Befehl legt die Datei components/ui/button-group.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, dropdown-menu und icon. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Knopfgruppe?
3 Varianten, Verbunden, Geteilter Knopf und Gestapelt.
Kann ich die Texte von Knopfgruppe übersetzen?
Ja. Knopfgruppe bringt 1 eigene Texte auf Deutsch und Englisch mit, etwa „Weitere Optionen“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Knopfgruppe einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Knopfgruppe mit aiw_components und installieren sie mit aiw_add @ai-website/button-group. Nenn im Prompt die Variante, etwa variant="outline", dann schreibt der Assistent deine Inhalte direkt in die Props.