Button Component für React
Fünf Stile, drei Größen, Icons, Laden, Gruppen, Icon-Knöpfe und neutrale Anmeldeknöpfe.
Eine von 6 Components in Buttons
Stile, Größen und Zustände
Der Button ist die meistgenutzte Component jeder Oberfläche, und er gibt den Ton für alles andere an. Dieser kommt in fünf Stilen, Primary, Secondary, Outline, Ghost und Destructive, in drei Größen, und er rendert einen echten Link, wenn du href übergibst, sonst einen echten Button.
Stile und Größen zeigen die Rangfolge auf einen Blick. „Mit Icons“ setzt ein Icon vor oder hinter das Label, im passenden Abstand. „Laden“ behält beim Arbeiten die Breite, das Layout springt also nicht. „Gruppen“ verbindet Buttons zu einer Reihe, Icon-Knöpfe sind quadratisch und verlangen ein aria-label, „Neutrale Anmeldung“ zeigt Weiter-Buttons für E-Mail und Passkey ganz ohne fremde Logos.
Gib jeder Ansicht einen Primary-Button. Halte die Größe in einer Reihe gleich, gemischte Größen wirken wie ein Fehler. Auf Touchscreens erreichen die Größen des Kits mindestens 44 Pixel Tap-Höhe, auch kleine Buttons bleiben gut treffbar.
Variants
Details
Was Knopf mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Buttons
- Bereich
- Elements
- Variants
- 7
- Version
- 1.0.1 · seit 1.0.0
- Datei
- components/ui/button.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
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- 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. 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, 190 Zeilen
'use client';
import { useRef } from 'react';
import { cn, layoutClasses, prefersReducedMotion } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive';
export type ButtonSize = 'sm' | 'md' | 'lg';
const VARIANT: Record<ButtonVariant, string> = {
primary:
'kit-shine bg-primary text-primary-foreground shadow-[inset_0_1px_0_rgb(255_255_255/0.18),var(--shadow-sm)] hover:bg-[color-mix(in_oklab,var(--primary)_88%,var(--background))]',
destructive:
'bg-destructive text-destructive-foreground shadow-[inset_0_1px_0_rgb(255_255_255/0.16),var(--shadow-sm)] hover:bg-[color-mix(in_oklab,var(--destructive)_88%,var(--background))]',
secondary: 'bg-secondary text-secondary-foreground shadow-[var(--highlight)] hover:bg-accent',
outline: 'border border-border-strong bg-background text-foreground shadow-[var(--shadow-sm)] hover:bg-accent',
ghost: 'text-foreground hover:bg-accent',
link: 'text-foreground underline-offset-4 hover:underline px-0! h-auto! min-h-0!',
};
const SIZE: Record<ButtonSize, string> = {
// Small buttons grow to 44 px on phones, the minimum for a finger
sm: 'h-9 min-h-9 gap-1.5 rounded-md px-3.5 text-[13px] max-sm:h-11 max-sm:min-h-11',
md: 'h-11 min-h-11 gap-2 rounded-lg px-5 text-sm',
lg: 'h-12 min-h-12 gap-2 rounded-lg px-6 text-[15px]',
};
/* ------------------------------------------------------------------ */
/* Press feedback */
/* ------------------------------------------------------------------ */
/**
* A short light at the point of the press, drawn with the Web Animations API
* so nothing lives in the React tree. glow: a white burst of light for filled main
* actions. ripple: a quiet ring of the text color for everything else.
* The layer sits behind the label, clips to the radius and removes itself.
* The element needs position relative and isolation (Button has both).
*/
export function pressLight(el: HTMLElement, clientX: number, clientY: number, kind: 'glow' | 'ripple' = 'ripple') {
if (prefersReducedMotion() || typeof el.animate !== 'function') return;
const r = el.getBoundingClientRect();
if (!r.width || !r.height) return;
const x = Math.min(Math.max(clientX - r.left, 0), r.width);
const y = Math.min(Math.max(clientY - r.top, 0), r.height);
const size = Math.ceil(Math.hypot(Math.max(x, r.width - x), Math.max(y, r.height - y)) * 2);
const clip = document.createElement('span');
clip.setAttribute('aria-hidden', 'true');
clip.style.cssText = 'position:absolute;inset:0;z-index:-1;border-radius:inherit;overflow:hidden;pointer-events:none;';
const dot = document.createElement('span');
dot.style.cssText = `position:absolute;left:${Math.round(x - size / 2)}px;top:${Math.round(y - size / 2)}px;width:${size}px;height:${size}px;border-radius:9999px;background:${
kind === 'glow'
? 'radial-gradient(circle closest-side, rgb(255 255 255 / 0.42), rgb(255 255 255 / 0.12) 55%, transparent)'
: 'color-mix(in oklab, currentColor 13%, transparent)'
};`;
clip.append(dot);
el.append(clip);
const anim = dot.animate(
[
{ transform: 'scale(0.08)', opacity: kind === 'glow' ? 1 : 0.9 },
{ transform: 'scale(1)', opacity: 0 },
],
{ duration: kind === 'glow' ? 700 : 560, easing: 'cubic-bezier(0.2,0.7,0.2,1)' },
);
const done = () => clip.remove();
anim.onfinish = done;
anim.oncancel = done;
}
/** Pointer press at the pointer, keyboard press (a click with detail 0) from the middle */
function usePress(kind: 'glow' | 'ripple' | null) {
return {
onPointerDown: (e: React.PointerEvent<HTMLElement>) => {
if (kind && e.button === 0) pressLight(e.currentTarget, e.clientX, e.clientY, kind);
},
onClick: (e: React.MouseEvent<HTMLElement>) => {
if (!kind || e.detail !== 0) return;
const r = e.currentTarget.getBoundingClientRect();
pressLight(e.currentTarget, r.left + r.width / 2, r.top + r.height / 2, kind);
},
};
}
/** Pulls its child a few pixels toward a mouse pointer. No effect on touch or with reduced motion. */
function MagneticWrap({ strength = 6, className, children }: { strength?: number; className?: string; children: React.ReactNode }) {
const ref = useRef<HTMLSpanElement>(null);
return (
<span
ref={ref}
onPointerMove={(e) => {
const el = ref.current;
if (!el || e.pointerType !== 'mouse' || prefersReducedMotion()) return;
const r = el.getBoundingClientRect();
const x = ((e.clientX - r.left) / r.width - 0.5) * 2 * strength;
const y = ((e.clientY - r.top) / r.height - 0.5) * 2 * strength;
el.style.transform = `translate3d(${x.toFixed(1)}px, ${y.toFixed(1)}px, 0)`;
}}
onPointerLeave={() => {
if (ref.current) ref.current.style.transform = '';
}}
className={cn('inline-flex transition-transform duration-300 ease-[var(--ease-kit)] motion-reduce:transition-none', className)}
>
{children}
</span>
);
}
/* ------------------------------------------------------------------ */
/* Button */
/* ------------------------------------------------------------------ */
interface Common {
variant?: ButtonVariant;
size?: ButtonSize;
/** Icon after the label, e.g. arrow-right. Moves a little on hover. */
icon?: IconName;
/** Icon before the label */
iconLeft?: IconName;
/** Pulls the button a few pixels toward the mouse. For the one main action of a section. */
magnetic?: boolean;
className?: string;
children?: React.ReactNode;
}
type AsLink = Common & { href: string; ref?: React.Ref<HTMLAnchorElement> } & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof Common>;
type AsButton = Common & { href?: undefined; ref?: React.Ref<HTMLButtonElement> } & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof Common>;
/**
* Button or link that looks like a button. With href it renders an <a>.
* A press scales it down a little and leaves a light at the press point:
* a glow on primary and destructive, a quiet ripple on the others.
*/
export function Button(props: AsLink | AsButton) {
const { variant = 'primary', size = 'md', icon, iconLeft, magnetic, className, children, ...rest } = props;
const press = usePress(variant === 'link' ? null : variant === 'primary' || variant === 'destructive' ? 'glow' : 'ripple');
const classes = cn(
'group/button relative isolate inline-flex shrink-0 select-none items-center justify-center whitespace-nowrap font-medium tracking-[-0.01em] transition-[background-color,color,box-shadow,transform,scale,translate] duration-200 ease-[var(--ease-kit)] active:scale-[0.97] active:duration-100 disabled:pointer-events-none disabled:opacity-50 motion-reduce:active:scale-100',
SIZE[size],
VARIANT[variant],
className,
);
const inner = (
<>
{iconLeft && <Icon name={iconLeft} size={size === 'sm' ? 15 : 17} />}
{children}
{icon && <Icon name={icon} size={size === 'sm' ? 15 : 17} className="transition-transform duration-200 group-hover/button:translate-x-0.5 motion-reduce:transition-none" />}
</>
);
let el: React.ReactNode;
if ('href' in rest && rest.href !== undefined) {
const link = rest as React.AnchorHTMLAttributes<HTMLAnchorElement>;
el = (
<a
{...link}
onPointerDown={(e) => {
press.onPointerDown(e);
link.onPointerDown?.(e);
}}
onClick={(e) => {
press.onClick(e);
link.onClick?.(e);
}}
className={classes}
>
{inner}
</a>
);
} else {
const { type = 'button', ...buttonRest } = rest as React.ButtonHTMLAttributes<HTMLButtonElement>;
el = (
<button
type={type}
{...buttonRest}
onPointerDown={(e) => {
press.onPointerDown(e);
buttonRest.onPointerDown?.(e);
}}
onClick={(e) => {
press.onClick(e);
buttonRest.onClick?.(e);
}}
className={classes}
>
{inner}
</button>
);
}
// The wrapper takes over the button's place in the layout (width, flex, grid, self), so a
// full-width magnetic button stays full width on phones
return magnetic ? <MagneticWrap className={layoutClasses(className)}>{el}</MagneticWrap> : el;
}Häufige Fragen
Wie installiere ich Knopf?
Mit aiw add @ai-website/button. Der Befehl legt die Datei components/ui/button.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Knopf?
7 Varianten, Stile, Größen, Mit Icons, Laden, Gruppen, Icon-Knöpfe und Neutrale Anmeldung. Du wählst sie mit der Prop variant, zum Beispiel variant="outline".
Kann ein KI-Assistent Knopf einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Knopf mit aiw_components und installieren sie mit aiw_add @ai-website/button. Nenn im Prompt die Variante, etwa variant="secondary|outline|ghost|link|destructive", dann schreibt der Assistent deine Inhalte direkt in die Props.