Schwebende Aktionen
Nach oben, Lesefortschritt, eine mobile Aktionsleiste, ein klebender Call to Action und ein Kontakt-Dock. Sie folgen dem Scrollen der Seite.
Eine von 3 Components in Menüleisten und Umschalter
Variants
Details
Was Schwebende Aktionen mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Menüleisten und Umschalter
- Bereich
- Elements
- Variants
- 5
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/floating-actions.tsx
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 9 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle FloatingActionsProps aus components/ui/floating-actions.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalFloatingActionsVariantback-to-topreading-progressmobile-action-barsticky-ctacontact-dock
thresholdoptionalnumberback-to-top, sticky-cta: pixels of scroll before it appears
showPercentoptionalbooleanback-to-top: show the percent read next to the arrow on hover and focus
titleoptionalstringreading-progress: the article title in the bar that slides in
minutesoptionalnumberreading-progress: reading time of the whole page in minutes
textoptionalstringsticky-cta: a second line under the title
actionoptional{ label: string; href: string }sticky-cta: the main and an optional second action
secondaryActionoptional{ label: string; href: string }dismissibleoptionalbooleansticky-cta: show a close button
actionsoptionalFloatingAction[]mobile-action-bar, contact-dock: the actions
statusoptionalstringmobile-action-bar, contact-dock: a live status, e.g. Open until 18:00
labeloptionalstringcontact-dock: the label of the closed button, collapses to the icon while scrolling down
headingoptionalstringcontact-dock: the heading of the open panel
Eingebaute Texte
Diese Texte schreibt Schwebende Aktionen selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 9 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| backToTop | Englisch Back to top | Deutsch Nach oben |
| percent | Englisch {n}% | Deutsch {n} % |
| minLeft | Englisch {n} min left | Deutsch noch {n} Min. |
| done | Englisch Finished | Deutsch Fertig gelesen |
| reading | Englisch Reading progress | Deutsch Lesefortschritt |
| close | Englisch Close | Deutsch Schließen |
| openContact | Englisch Open contact options | Deutsch Kontaktwege öffnen |
| closeContact | Englisch Close contact options | Deutsch Kontaktwege schließen |
| actions | Englisch Quick actions | Deutsch Schnellaktionen |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Schwebende Aktionen reagiert selbst auf die Tasten Escape.
- Setzt aria-controls, aria-expanded, aria-label, aria-valuemax, aria-valuemin und aria-valuenow, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rollen dialog, progressbar und region.
- Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
- Bewegt sich mit dem Scrollen, der Fortschritt folgt der Position auf der Seite.
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 3 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 485 Zeilen
'use client';
import { createContext, useContext, useEffect, useId, useRef, useState } from 'react';
import { cn, prefersReducedMotion, round, useTexts } from '@/lib/kit';
import { Button } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
/*
* Things that float over a page and follow the scroll: back to top, reading
* progress, the action bar of a local business, a sticky call to action and
* a contact dock. They sit `fixed` over the window. Inside a <ScrollViewport>
* (a framed page that scrolls on its own, for previews and mockups) they sit
* `absolute` over that frame and follow its scroll instead.
*
* Server and no-JS render the shown state, the browser hides what the scroll
* position does not need yet. Only transform and opacity move.
*/
export type FloatingActionsVariant = 'back-to-top' | 'reading-progress' | 'mobile-action-bar' | 'sticky-cta' | 'contact-dock';
export interface FloatingAction {
icon: IconName;
label: string;
href: string;
/** The one main action, filled with the accent */
primary?: boolean;
/** contact-dock: a second line, e.g. the number or the reply time */
detail?: string;
}
export interface FloatingActionsProps {
variant?: FloatingActionsVariant;
/** back-to-top, sticky-cta: pixels of scroll before it appears */
threshold?: number;
/** back-to-top: show the percent read next to the arrow on hover and focus */
showPercent?: boolean;
/** reading-progress: the article title in the bar that slides in */
title?: string;
/** reading-progress: reading time of the whole page in minutes */
minutes?: number;
/** sticky-cta: a second line under the title */
text?: string;
/** sticky-cta: the main and an optional second action */
action?: { label: string; href: string };
secondaryAction?: { label: string; href: string };
/** sticky-cta: show a close button */
dismissible?: boolean;
/** mobile-action-bar, contact-dock: the actions */
actions?: FloatingAction[];
/** mobile-action-bar, contact-dock: a live status, e.g. Open until 18:00 */
status?: string;
/** contact-dock: the label of the closed button, collapses to the icon while scrolling down */
label?: string;
/** contact-dock: the heading of the open panel */
heading?: string;
labels?: Partial<Texts>;
className?: string;
}
const TEXTS = {
en: {
backToTop: 'Back to top',
percent: '{n}%',
minLeft: '{n} min left',
done: 'Finished',
reading: 'Reading progress',
close: 'Close',
openContact: 'Open contact options',
closeContact: 'Close contact options',
actions: 'Quick actions',
},
de: {
backToTop: 'Nach oben',
percent: '{n} %',
minLeft: 'noch {n} Min.',
done: 'Fertig gelesen',
reading: 'Lesefortschritt',
close: 'Schließen',
openContact: 'Kontaktwege öffnen',
closeContact: 'Kontaktwege schließen',
actions: 'Schnellaktionen',
},
};
type Texts = (typeof TEXTS)['en'];
/* ------------------------------------------------------------------ */
/* Scroll source: the window or a ScrollViewport */
/* ------------------------------------------------------------------ */
const ViewportContext = createContext<React.RefObject<HTMLDivElement | null> | null>(null);
/**
* A framed page that scrolls on its own. Floating actions inside `children`
* sit over the frame and follow its scroll. Fixed pixel height, never vh, so
* it can live inside an auto-sized iframe.
* <ScrollViewport height={620} page={<YourPage />}><FloatingActions … /></ScrollViewport>
*/
export function ScrollViewport({ page, height = 620, initialScroll = 0, className, children }: { page: React.ReactNode; height?: number; initialScroll?: number; className?: string; children?: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (ref.current && initialScroll) ref.current.scrollTop = initialScroll;
}, [initialScroll]);
return (
<ViewportContext.Provider value={ref}>
<div data-clip-ok className={cn('relative isolate overflow-hidden rounded-2xl border border-border bg-background shadow-[var(--highlight),var(--shadow-lg)]', className)} style={{ height }}>
<div ref={ref} tabIndex={0} className="kit-scrollbar absolute inset-0 overflow-y-auto overscroll-contain focus-visible:outline-none">
{page}
</div>
{children}
</div>
</ViewportContext.Provider>
);
}
interface ScrollState {
y: number;
progress: number;
down: boolean;
/** false on the server and before the first measurement: show everything */
ready: boolean;
}
function useScroll() {
const vp = useContext(ViewportContext);
const [state, setState] = useState<ScrollState>({ y: 0, progress: 1, down: false, ready: false });
useEffect(() => {
const el = vp?.current ?? null;
const source: HTMLElement | Window = el ?? window;
let last = el ? el.scrollTop : window.scrollY;
let frame = 0;
let settled = false;
const read = () => {
frame = 0;
const y = el ? el.scrollTop : window.scrollY;
const max = el ? el.scrollHeight - el.clientHeight : document.documentElement.scrollHeight - window.innerHeight;
const down = !settled || Math.abs(y - last) < 4 ? undefined : y > last;
last = y;
setState((s) => ({ y, progress: max > 0 ? Math.min(1, Math.max(0, y / max)) : 1, down: down ?? s.down, ready: true }));
};
const onScroll = () => {
if (!frame) frame = requestAnimationFrame(read);
};
read();
// A viewport may set its start position after this effect: read once more, without a direction
const t = window.setTimeout(() => {
last = el ? el.scrollTop : window.scrollY;
settled = true;
read();
}, 120);
source.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onScroll);
return () => {
cancelAnimationFrame(frame);
window.clearTimeout(t);
source.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onScroll);
};
}, [vp]);
const scrollTop = () => {
const behavior: ScrollBehavior = prefersReducedMotion() ? 'auto' : 'smooth';
(vp?.current ?? window).scrollTo({ top: 0, behavior });
};
return { ...state, contained: !!vp, scrollTop };
}
const MOVE = 'transition-[transform,opacity] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none';
/* ------------------------------------------------------------------ */
/* back-to-top */
/* ------------------------------------------------------------------ */
function BackToTop(p: FloatingActionsProps & { t: Texts }) {
const { t } = p;
const s = useScroll();
const shown = !s.ready || s.y > (p.threshold ?? 400);
const pct = Math.round(s.progress * 100);
const R = 21;
const C = round(2 * Math.PI * R);
return (
<a
href="#top"
onClick={(e) => {
e.preventDefault();
s.scrollTop();
}}
aria-label={t.backToTop}
tabIndex={shown ? undefined : -1}
aria-hidden={shown ? undefined : true}
className={cn(
s.contained ? 'absolute' : 'fixed',
'group/top bottom-5 right-5 z-40 flex h-12 items-center rounded-full border border-border bg-card/90 text-foreground shadow-[var(--highlight),var(--shadow-lg)] backdrop-blur-md sm:bottom-6 sm:right-6',
MOVE,
shown ? 'translate-y-0 scale-100 opacity-100' : 'pointer-events-none translate-y-4 scale-90 opacity-0',
p.className,
)}
>
{p.showPercent && (
<span className="grid max-w-0 overflow-hidden whitespace-nowrap text-[13px] font-medium tabular-nums text-muted-foreground opacity-0 transition-[max-width,opacity,padding] duration-500 ease-[var(--ease-kit)] group-hover/top:max-w-24 group-hover/top:pl-4 group-hover/top:opacity-100 group-focus-visible/top:max-w-24 group-focus-visible/top:pl-4 group-focus-visible/top:opacity-100 max-sm:hidden">
{t.percent.replace('{n}', String(pct))}
</span>
)}
<span className="relative grid size-12 place-items-center">
<svg viewBox="0 0 48 48" className="absolute inset-0 -rotate-90" aria-hidden>
<circle cx="24" cy="24" r={R} fill="none" stroke="var(--border)" strokeWidth="2" />
<circle cx="24" cy="24" r={R} fill="none" stroke="var(--primary)" strokeWidth="2" strokeLinecap="round" strokeDasharray={C} strokeDashoffset={round(C * (1 - s.progress))} className="transition-[stroke-dashoffset] duration-200" />
</svg>
<Icon name="arrow-up" size={18} className="transition-transform duration-300 ease-[var(--ease-kit)] group-hover/top:-translate-y-0.5" />
</span>
</a>
);
}
/* ------------------------------------------------------------------ */
/* reading-progress */
/* ------------------------------------------------------------------ */
function ReadingProgress(p: FloatingActionsProps & { t: Texts }) {
const { t } = p;
const s = useScroll();
const shown = !s.ready || s.y > (p.threshold ?? 240);
const left = p.minutes ? Math.max(0, Math.ceil(p.minutes * (1 - s.progress))) : null;
const pos = s.contained ? 'absolute' : 'fixed';
return (
<>
<div role="progressbar" aria-label={t.reading} aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(s.progress * 100)} className={cn(pos, 'inset-x-0 top-0 z-50 h-[3px] bg-transparent')}>
<div className="h-full origin-left bg-primary shadow-[0_0_12px_var(--glow)] transition-transform duration-150 motion-reduce:transition-none" style={{ transform: `scaleX(${round(s.progress, 4)})` }} />
</div>
{p.title && (
<div aria-hidden={shown ? undefined : true} className={cn(pos, 'inset-x-0 top-0 z-40 border-b border-border bg-background/85 backdrop-blur-md', MOVE, shown ? 'translate-y-0 opacity-100' : 'pointer-events-none -translate-y-full opacity-0', p.className)}>
<div className="mx-auto flex h-14 max-w-[calc(var(--container-kit)+4rem)] items-center gap-4 px-4 sm:px-6 lg:px-8">
<p className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">{p.title}</p>
{left !== null && (
<span className="flex shrink-0 items-center gap-2 text-[13px] tabular-nums text-muted-foreground">
<span key={left} className="kit-rise">
{left === 0 ? t.done : t.minLeft.replace('{n}', String(left))}
</span>
</span>
)}
<a
href="#top"
onClick={(e) => {
e.preventDefault();
s.scrollTop();
}}
tabIndex={shown ? undefined : -1}
aria-label={t.backToTop}
className="grid size-10 shrink-0 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground max-sm:size-11"
>
<Icon name="arrow-up" size={17} />
</a>
</div>
</div>
)}
</>
);
}
/* ------------------------------------------------------------------ */
/* mobile-action-bar */
/* ------------------------------------------------------------------ */
function StatusDot({ text }: { text: string }) {
return (
<span className="flex items-center gap-2 whitespace-nowrap text-[13px] font-medium text-foreground">
<span className="relative flex size-2">
<span className="absolute inset-0 rounded-full bg-success opacity-60 motion-safe:animate-ping" />
<span className="relative size-2 rounded-full bg-success" />
</span>
{text}
</span>
);
}
function MobileActionBar(p: FloatingActionsProps & { t: Texts }) {
const { t } = p;
const s = useScroll();
const actions = p.actions ?? [];
// Hides while reading down, comes back on the first move up
const hidden = s.ready && s.down && s.y > 160;
return (
<nav
aria-label={t.actions}
className={cn(
s.contained ? 'absolute' : 'fixed',
'z-40 inset-x-0 bottom-0 sm:inset-x-auto sm:bottom-6 sm:left-1/2 sm:-translate-x-1/2',
MOVE,
hidden ? 'translate-y-[120%] sm:translate-y-[calc(100%+2rem)]' : 'translate-y-0',
p.className,
)}
>
{/* Phones: a full-width bar with big targets. Wider screens: a floating pill with the status. */}
<div className="border-t border-border bg-background/92 pb-[env(safe-area-inset-bottom)] shadow-[0_-8px_24px_-12px_hsl(var(--shadow-color)/0.25)] backdrop-blur-md sm:flex sm:items-center sm:gap-2 sm:rounded-2xl sm:border sm:bg-card/92 sm:p-2 sm:shadow-[var(--highlight),var(--shadow-lg)]">
{p.status && (
<span className="hidden pl-3 pr-2 sm:block">
<StatusDot text={p.status} />
</span>
)}
<ul className="grid sm:flex sm:gap-1.5" style={{ gridTemplateColumns: `repeat(${Math.max(1, actions.length)}, minmax(0, 1fr))` }}>
{actions.map((a, i) => (
<li key={i}>
<a
href={a.href}
className={cn(
'group/act flex h-16 flex-col items-center justify-center gap-1 text-[12px] font-medium transition-colors sm:h-11 sm:flex-row sm:gap-2 sm:rounded-xl sm:px-4 sm:text-[14px]',
a.primary ? 'bg-primary text-primary-foreground sm:shadow-[inset_0_1px_0_rgb(255_255_255/0.18),var(--shadow-sm)]' : 'text-foreground hover:bg-accent',
)}
>
<Icon name={a.icon} size={19} className="transition-transform duration-300 ease-[var(--ease-kit)] group-hover/act:-translate-y-0.5 sm:size-[17px] sm:group-hover/act:translate-y-0 sm:group-hover/act:scale-110" />
<span className="whitespace-nowrap">{a.label}</span>
</a>
</li>
))}
</ul>
</div>
</nav>
);
}
/* ------------------------------------------------------------------ */
/* sticky-cta */
/* ------------------------------------------------------------------ */
function StickyCta(p: FloatingActionsProps & { t: Texts }) {
const { t } = p;
const s = useScroll();
const [closed, setClosed] = useState(false);
const shown = !closed && (!s.ready || s.y > (p.threshold ?? 520));
const pos = s.contained ? 'absolute' : 'fixed';
return (
<div
role="region"
aria-label={p.title}
aria-hidden={shown ? undefined : true}
inert={!shown}
className={cn(
pos,
'z-40 inset-x-0 bottom-0 border-t border-border bg-background/90 shadow-[0_-10px_30px_-14px_hsl(var(--shadow-color)/0.3)] backdrop-blur-md lg:bottom-auto lg:top-0 lg:border-b lg:border-t-0 lg:shadow-[0_10px_30px_-18px_hsl(var(--shadow-color)/0.35)]',
MOVE,
shown ? 'translate-y-0 opacity-100' : 'pointer-events-none translate-y-full opacity-0 lg:-translate-y-full',
p.className,
)}
>
<div className="mx-auto flex max-w-[calc(var(--container-kit)+4rem)] items-center gap-3 px-4 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom))] sm:gap-5 sm:px-6 lg:px-8 lg:py-3">
<div className="min-w-0 flex-1">
<p className="line-clamp-2 text-[14px] font-semibold leading-snug text-foreground sm:truncate sm:text-[14.5px]">{p.title}</p>
{p.text && <p className="truncate text-[13px] text-muted-foreground max-sm:hidden">{p.text}</p>}
</div>
{p.secondaryAction && (
<Button href={p.secondaryAction.href} variant="ghost" size="sm" className="max-md:hidden">
{p.secondaryAction.label}
</Button>
)}
{p.action && (
<Button href={p.action.href} size="sm" icon="arrow-right" className="shrink-0">
{p.action.label}
</Button>
)}
{p.dismissible && (
<button type="button" onClick={() => setClosed(true)} aria-label={t.close} className="-mr-1 grid size-11 shrink-0 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground sm:size-9">
<Icon name="x" size={17} />
</button>
)}
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
/* contact-dock */
/* ------------------------------------------------------------------ */
function ContactDock(p: FloatingActionsProps & { t: Texts }) {
const { t } = p;
const s = useScroll();
const [open, setOpen] = useState(false);
const id = useId();
const rootRef = useRef<HTMLDivElement>(null);
const toggleRef = useRef<HTMLButtonElement>(null);
const actions = p.actions ?? [];
// The label folds away while reading down and returns at the top or on the way up
const compact = s.ready && s.y > 120 && s.down && !open;
useEffect(() => {
if (!open) return;
rootRef.current?.querySelector<HTMLElement>('[data-dock-item]')?.focus();
const onKey = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
setOpen(false);
toggleRef.current?.focus();
};
const onDown = (e: PointerEvent) => {
if (!rootRef.current?.contains(e.target as Node)) setOpen(false);
};
document.addEventListener('keydown', onKey);
document.addEventListener('pointerdown', onDown);
return () => {
document.removeEventListener('keydown', onKey);
document.removeEventListener('pointerdown', onDown);
};
}, [open]);
return (
<div ref={rootRef} className={cn(s.contained ? 'absolute' : 'fixed', 'bottom-5 right-5 z-40 flex flex-col items-end gap-3 sm:bottom-6 sm:right-6', p.className)}>
<div
id={id}
role="dialog"
aria-label={p.heading ?? p.label}
inert={!open}
className={cn(
'w-[min(20rem,calc(100vw-2.5rem))] origin-bottom-right rounded-2xl border border-border bg-popover p-2 shadow-[var(--highlight),var(--shadow-lg)]',
MOVE,
open ? 'translate-y-0 scale-100 opacity-100' : 'pointer-events-none translate-y-3 scale-95 opacity-0',
)}
>
{(p.heading || p.status) && (
<div className="px-3 pb-2 pt-2.5">
{p.heading && <p className="text-[15px] font-semibold text-foreground">{p.heading}</p>}
{p.status && (
<div className="mt-1.5">
<StatusDot text={p.status} />
</div>
)}
</div>
)}
<ul className="space-y-0.5">
{actions.map((a, i) => (
<li key={i} className={cn(MOVE, open ? 'translate-y-0 opacity-100' : 'translate-y-2 opacity-0')} style={{ transitionDelay: open ? `${80 + i * 50}ms` : '0ms' }}>
<a data-dock-item href={a.href} className="group/item flex min-h-14 items-center gap-3 rounded-xl px-3 py-2 transition-colors hover:bg-accent focus-visible:bg-accent">
<span className={cn('grid size-10 shrink-0 place-items-center rounded-xl transition-transform duration-300 ease-[var(--ease-kit)] group-hover/item:scale-105', a.primary ? 'bg-primary text-primary-foreground' : 'bg-muted text-foreground')}>
<Icon name={a.icon} size={18} />
</span>
<span className="min-w-0 flex-1">
<span className="block text-[14.5px] font-medium text-foreground">{a.label}</span>
{a.detail && <span className="block truncate text-[13px] text-muted-foreground">{a.detail}</span>}
</span>
<Icon name="arrow-right" size={15} className="text-subtle-foreground opacity-0 transition-[opacity,transform] duration-300 group-hover/item:translate-x-0.5 group-hover/item:opacity-100" />
</a>
</li>
))}
</ul>
</div>
<button
ref={toggleRef}
type="button"
aria-expanded={open}
aria-controls={id}
aria-label={open ? t.closeContact : p.label ? undefined : t.openContact}
onClick={() => setOpen((v) => !v)}
className="kit-shine group/dock relative flex h-14 items-center rounded-full bg-primary pl-4 pr-4 text-primary-foreground shadow-[inset_0_1px_0_rgb(255_255_255/0.18),var(--shadow-lg)] transition-transform duration-200 ease-[var(--ease-kit)] active:scale-95"
>
<span className="relative grid size-6 place-items-center">
<Icon name="chat" size={22} className={cn('absolute transition-[transform,opacity] duration-300 ease-[var(--ease-kit)]', open ? 'rotate-90 scale-50 opacity-0' : 'opacity-100')} />
<Icon name="x" size={22} className={cn('absolute transition-[transform,opacity] duration-300 ease-[var(--ease-kit)]', open ? 'opacity-100' : '-rotate-90 scale-50 opacity-0')} />
</span>
{p.label && (
<span className={cn('grid overflow-hidden whitespace-nowrap text-[15px] font-medium transition-[max-width,opacity,padding] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', compact || open ? 'max-w-0 pl-0 opacity-0' : 'max-w-48 pl-2.5 pr-1 opacity-100')}>{p.label}</span>
)}
</button>
</div>
);
}
/* ------------------------------------------------------------------ */
/* FloatingActions */
/* ------------------------------------------------------------------ */
const VARIANTS: Record<FloatingActionsVariant, (p: FloatingActionsProps & { t: Texts }) => React.ReactNode> = {
'back-to-top': BackToTop,
'reading-progress': ReadingProgress,
'mobile-action-bar': MobileActionBar,
'sticky-cta': StickyCta,
'contact-dock': ContactDock,
};
/**
* Helpers that float over the page and follow the scroll.
* <FloatingActions variant="back-to-top" showPercent />
* <FloatingActions variant="mobile-action-bar" actions={[{ icon: 'phone', label: 'Call', href: 'tel:…' }]} />
*/
export function FloatingActions({ variant = 'back-to-top', labels, ...props }: FloatingActionsProps) {
const t = useTexts(TEXTS, labels);
const View = VARIANTS[variant] ?? BackToTop;
return <View {...props} t={t} />;
}Häufige Fragen
Wie installiere ich Schwebende Aktionen?
Mit aiw add @ai-website/floating-actions. Der Befehl legt die Datei components/ui/floating-actions.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button und icon. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Schwebende Aktionen?
5 Varianten, Nach oben, Lesefortschritt, Aktionsleiste, Klebender Call to Action und Kontakt-Dock. Du wählst sie mit der Prop variant, zum Beispiel variant="reading-progress".
Kann ich die Texte von Schwebende Aktionen übersetzen?
Ja. Schwebende Aktionen bringt 9 eigene Texte auf Deutsch und Englisch mit, etwa „Nach oben“, „Fertig gelesen“ und „Lesefortschritt“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Schwebende Aktionen einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Schwebende Aktionen mit aiw_components und installieren sie mit aiw_add @ai-website/floating-actions. Nenn im Prompt die Variante, etwa variant="back-to-top", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Schwebende Aktionen ohne Code anpassen?
Ja, im Studio. 4 von 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden FloatingActions-Code zum Kopieren.