"use client";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import { animate, motion, useMotionValue } from "motion/react";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonThemeClassName =
"[--button-destructive:#dc2626] [--button-input:#e3e7ec] [--button-primary-foreground:#ffffff] [--button-ring:rgba(17,17,17,0.16)] [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] [--button-secondary:#eceff3] [--button-secondary-foreground:#111111] dark:[--button-destructive:#f87171] dark:[--button-input:#2b2a25] dark:[--button-primary-foreground:#111111] dark:[--button-ring:rgba(246,243,236,0.18)] dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_30%)] dark:[--button-secondary:#2a2a27] dark:[--button-secondary-foreground:#f6f3ec]";
const buttonCornerClassName =
"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]";
const buttonCornerXsClassName =
"rounded-[min(var(--radius-md),10px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[12px]";
const buttonCornerSmClassName =
"rounded-[min(var(--radius-md),12px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[14px]";
const buttonGroupCornerClassName =
"in-data-[slot=button-group]:rounded-lg in-data-[slot=button-group]:supports-[corner-shape:squircle]:corner-squircle in-data-[slot=button-group]:supports-[corner-shape:squircle]:rounded-[11px]";
const SUCCESS_HOLD_MS = 1000;
const FLUID_WIDTH = {
type: "spring" as const,
stiffness: 210,
damping: 28,
mass: 1.08,
};
const FLUID_CONTENT = {
type: "spring" as const,
stiffness: 340,
damping: 32,
mass: 0.92,
};
const FLUID_ICON = {
type: "spring" as const,
stiffness: 400,
damping: 30,
mass: 0.76,
};
const FLUID_REVEAL = {
type: "spring" as const,
stiffness: 260,
damping: 30,
mass: 0.98,
};
const FLUID_TAP = {
type: "spring" as const,
stiffness: 520,
damping: 34,
mass: 0.72,
};
const NO_TRANSITION = { duration: 0 } as const;
type FlowPhase = "idle" | "loading" | "success" | "shrinking";
function getWidthTarget(
phase: FlowPhase,
widths: { idle: number; loading: number; success: number }
) {
switch (phase) {
case "loading":
return widths.loading;
case "success":
return widths.success;
default:
return widths.idle;
}
}
const SIZE_METRICS = {
xs: { height: 24, paddingX: 8 },
sm: { height: 28, paddingX: 10 },
default: { height: 32, paddingX: 10 },
lg: { height: 40, paddingX: 16 },
} as const;
const LINK_PADDING_X = {
xs: 6,
sm: 8,
default: 8,
lg: 10,
} as const;
const ICON_SLOT_CLASS = {
xs: "size-3",
sm: "size-3.5",
default: "size-4",
lg: "size-4",
} as const;
const BUSY_GAP_CLASS = {
xs: "gap-1",
sm: "gap-1",
default: "gap-1.5",
lg: "gap-1.5",
} as const;
const SHRINK_FALLBACK_MS = 900;
const fluxButtonVariants = cva(
cn(
buttonThemeClassName,
buttonCornerClassName,
"relative inline-flex shrink-0 cursor-pointer touch-manipulation select-none items-center justify-center overflow-hidden whitespace-nowrap border border-transparent bg-clip-padding font-medium text-sm no-underline outline-none [-webkit-tap-highlight-color:transparent] focus-visible:border-[color:var(--button-ring)] focus-visible:ring-3 focus-visible:ring-[color:color-mix(in_oklch,var(--button-ring),transparent_50%)] active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
),
{
variants: {
variant: {
default:
"visited:!text-primary-foreground hover:!text-primary-foreground focus-visible:!text-primary-foreground active:!text-primary-foreground bg-primary text-primary-foreground hover:bg-primary dark:[--button-ripple:color-mix(in_oklch,#111111,transparent_10%)] [&_svg]:text-primary-foreground",
outline:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground border-border bg-background text-foreground hover:bg-accent/60 dark:border-[color:var(--button-input)] dark:bg-[color-mix(in_oklch,var(--button-input),transparent_70%)] dark:hover:bg-[color-mix(in_oklch,var(--button-input),transparent_50%)]",
secondary:
"visited:!text-[color:var(--button-secondary-foreground)] hover:!text-[color:var(--button-secondary-foreground)] focus-visible:!text-[color:var(--button-secondary-foreground)] active:!text-[color:var(--button-secondary-foreground)] bg-[color:var(--button-secondary)] text-[color:var(--button-secondary-foreground)] hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_7%)] dark:hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_11%)] [&_svg]:text-[color:var(--button-secondary-foreground)]",
ghost:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground text-foreground hover:bg-accent/60 dark:hover:bg-muted/50",
destructive:
"visited:!text-[color:var(--button-destructive)] hover:!text-[color:var(--button-destructive)] focus-visible:!text-[color:var(--button-destructive)] active:!text-[color:var(--button-destructive)] bg-[color-mix(in_oklch,var(--button-destructive),transparent_90%)] text-[color:var(--button-destructive)] hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] focus-visible:border-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] dark:hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_70%)]",
link: "visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground text-foreground underline-offset-4 shadow-none hover:underline active:translate-y-0",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: cn(
"h-6 gap-1 px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
buttonGroupCornerClassName,
buttonCornerXsClassName
),
sm: cn(
"h-7 gap-1 px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
buttonGroupCornerClassName,
buttonCornerSmClassName
),
lg: "h-10 gap-1.5 px-4 text-base leading-5 has-data-[icon=inline-end]:pr-3.5 has-data-[icon=inline-start]:pl-3.5",
},
},
compoundVariants: [
{
variant: "link",
size: "default",
class: "h-8 px-2 text-sm leading-5",
},
{
variant: "link",
size: "lg",
class: "h-10 px-2.5 text-base leading-5",
},
{
variant: "link",
size: "sm",
class: "h-7 px-2 text-[0.8rem]",
},
{
variant: "link",
size: "xs",
class: "h-6 px-1.5 text-xs",
},
],
defaultVariants: {
size: "default",
variant: "default",
},
}
);
type ButtonHTMLAttributesForMotion = Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
| "onAnimationEnd"
| "onAnimationIteration"
| "onAnimationStart"
| "onDrag"
| "onDragEnd"
| "onDragEnter"
| "onDragExit"
| "onDragLeave"
| "onDragOver"
| "onDragStart"
| "onDrop"
>;
type ButtonRenderProps = React.HTMLAttributes<HTMLButtonElement> & {
className?: string;
ref?: React.Ref<HTMLButtonElement>;
style?: React.CSSProperties;
};
function setRef<T>(ref: React.Ref<T> | undefined, value: T) {
if (typeof ref === "function") {
ref(value);
return;
}
if (ref) {
(ref as React.MutableRefObject<T>).current = value;
}
}
function resolvePrimitiveButtonProps(buttonProps: ButtonRenderProps) {
const {
children: _buttonChildren,
className: primitiveClassName,
onAnimationEnd: _onAnimationEnd,
onAnimationIteration: _onAnimationIteration,
onAnimationStart: _onAnimationStart,
onDrag: _onDrag,
onDragEnd: _onDragEnd,
onDragEnter: _onDragEnter,
onDragExit: _onDragExit,
onDragLeave: _onDragLeave,
onDragOver: _onDragOver,
onDragStart: _onDragStart,
onDrop: _onDrop,
ref: primitiveRef,
style: primitiveStyle,
...resolvedButtonProps
} = buttonProps;
return {
primitiveClassName,
primitiveRef,
primitiveStyle,
resolvedButtonProps,
};
}
type FluxButtonProps = Omit<ButtonHTMLAttributesForMotion, "type"> &
VariantProps<typeof fluxButtonVariants> & {
idleLabel: string;
loadingLabel: string;
successLabel: string;
successIcon?: React.ReactNode;
onAction: () => void | Promise<void>;
successHold?: number;
type?: "button" | "submit";
};
function getLayoutMetrics(
size: NonNullable<FluxButtonProps["size"]>,
variant: NonNullable<FluxButtonProps["variant"]>
) {
const metrics = SIZE_METRICS[size];
return {
height: metrics.height,
paddingX: variant === "link" ? LINK_PADDING_X[size] : metrics.paddingX,
};
}
function useMeasuredWidths(paddingX: number, minWidth: number) {
const measureRef = React.useRef<HTMLDivElement>(null);
const [widths, setWidths] = React.useState({
idle: minWidth,
loading: minWidth,
success: minWidth,
});
React.useLayoutEffect(() => {
const root = measureRef.current;
if (!root) {
return;
}
const readWidth = (selector: string) => {
const node = root.querySelector<HTMLElement>(selector);
return Math.max(minWidth, (node?.offsetWidth ?? 0) + paddingX * 2);
};
const measure = () => {
setWidths({
idle: readWidth('[data-width="idle"]'),
loading: readWidth('[data-width="loading"]'),
success: readWidth('[data-width="success"]'),
});
};
measure();
const resizeObserver = new ResizeObserver(measure);
resizeObserver.observe(root);
return () => resizeObserver.disconnect();
}, [minWidth, paddingX]);
return { measureRef, widths };
}
type FlowContentProps = {
animateEnabled: boolean;
labels: { idle: string; loading: string; success: string };
phase: FlowPhase;
size: NonNullable<FluxButtonProps["size"]>;
successIcon?: React.ReactNode;
};
function useFlowContentMotion(animateEnabled: boolean, phase: FlowPhase) {
const motionEnabled = animateEnabled;
const contentTransition = motionEnabled ? FLUID_CONTENT : NO_TRANSITION;
const iconTransition = motionEnabled ? FLUID_ICON : NO_TRANSITION;
const shrinkTransition = motionEnabled ? FLUID_REVEAL : NO_TRANSITION;
const isShrinking = phase === "shrinking";
return {
contentTransition,
iconTransition,
idleTransition: isShrinking ? shrinkTransition : contentTransition,
busyTransition: isShrinking ? NO_TRANSITION : contentTransition,
};
}
function FlowIdleLayer({
idleLabel,
idleOpacity,
idleTransition,
isIdle,
isShrinking,
}: {
idleLabel: string;
idleOpacity: number;
idleTransition:
| typeof FLUID_CONTENT
| typeof FLUID_REVEAL
| typeof NO_TRANSITION;
isIdle: boolean;
isShrinking: boolean;
}) {
return (
<motion.span
animate={{ opacity: idleOpacity }}
aria-hidden={!(isIdle || isShrinking)}
className="absolute inset-0 inline-flex items-center justify-center whitespace-nowrap"
initial={false}
style={{ pointerEvents: "none", zIndex: idleOpacity > 0.5 ? 1 : 0 }}
transition={idleTransition}
>
{idleLabel}
</motion.span>
);
}
function FlowIconSlot({
iconSlotClassName,
iconTransition,
isLoading,
isSuccess,
showSuccessIcon,
successIcon,
}: {
iconSlotClassName: string;
iconTransition: typeof FLUID_ICON | typeof NO_TRANSITION;
isLoading: boolean;
isSuccess: boolean;
showSuccessIcon: boolean;
successIcon?: React.ReactNode;
}) {
return (
<span
className={cn("relative shrink-0 [&_svg]:size-[1em]", iconSlotClassName)}
>
<motion.span
animate={{
opacity: isLoading ? 1 : 0,
rotate: isLoading ? 0 : 72,
scale: isLoading ? 1 : 0.82,
}}
aria-hidden={!isLoading}
className="absolute inset-0 inline-flex items-center justify-center"
initial={false}
transition={iconTransition}
>
<Loader2
aria-hidden
className={cn(iconSlotClassName, isLoading && "animate-spin")}
/>
</motion.span>
{showSuccessIcon ? (
<motion.span
animate={{
opacity: isSuccess ? 1 : 0,
rotate: isSuccess ? 0 : -72,
scale: isSuccess ? 1 : 0.82,
}}
aria-hidden={!isSuccess}
className="absolute inset-0 inline-flex items-center justify-center"
initial={false}
transition={iconTransition}
>
{successIcon}
</motion.span>
) : null}
</span>
);
}
function FlowBusyLabels({
contentTransition,
isLoading,
isSuccess,
loadingLabel,
successLabel,
}: {
contentTransition: typeof FLUID_CONTENT | typeof NO_TRANSITION;
isLoading: boolean;
isSuccess: boolean;
loadingLabel: string;
successLabel: string;
}) {
return (
<span className="relative inline-grid [grid-template-columns:max-content] [grid-template-rows:1.25em]">
<motion.span
animate={{ opacity: isLoading ? 1 : 0 }}
aria-hidden={!isLoading}
className="col-start-1 row-start-1 inline-flex items-center whitespace-nowrap"
initial={false}
style={{ zIndex: isLoading ? 1 : 0 }}
transition={contentTransition}
>
{loadingLabel}
</motion.span>
<motion.span
animate={{ opacity: isSuccess ? 1 : 0 }}
aria-hidden={!isSuccess}
className="col-start-1 row-start-1 inline-flex items-center whitespace-nowrap"
initial={false}
style={{ zIndex: isSuccess ? 1 : 0 }}
transition={contentTransition}
>
{successLabel}
</motion.span>
</span>
);
}
function FlowContent({
animateEnabled,
labels,
phase,
size,
successIcon,
}: FlowContentProps) {
const { busyTransition, contentTransition, iconTransition, idleTransition } =
useFlowContentMotion(animateEnabled, phase);
const isIdle = phase === "idle";
const isShrinking = phase === "shrinking";
const isLoading = phase === "loading";
const isSuccess = phase === "success";
const isBusy = isLoading || isSuccess;
const showSuccessIcon = Boolean(successIcon);
const showIconSlot = isLoading || (isSuccess && showSuccessIcon);
const iconSlotClassName = ICON_SLOT_CLASS[size];
const busyGapClassName = BUSY_GAP_CLASS[size];
const idleOpacity = isIdle || isShrinking ? 1 : 0;
const busyOpacity = isBusy ? 1 : 0;
return (
<span className="relative h-[1.25em] w-full leading-none">
<FlowIdleLayer
idleLabel={labels.idle}
idleOpacity={idleOpacity}
idleTransition={idleTransition}
isIdle={isIdle}
isShrinking={isShrinking}
/>
<motion.span
animate={{ opacity: busyOpacity }}
aria-hidden={!isBusy}
className={cn(
"absolute inset-0 inline-flex items-center justify-center whitespace-nowrap",
showIconSlot && busyGapClassName
)}
initial={false}
style={{ pointerEvents: "none", zIndex: busyOpacity > 0.5 ? 1 : 0 }}
transition={busyTransition}
>
{showIconSlot ? (
<FlowIconSlot
iconSlotClassName={iconSlotClassName}
iconTransition={iconTransition}
isLoading={isLoading}
isSuccess={isSuccess}
showSuccessIcon={showSuccessIcon}
successIcon={successIcon}
/>
) : null}
<FlowBusyLabels
contentTransition={contentTransition}
isLoading={isLoading}
isSuccess={isSuccess}
loadingLabel={labels.loading}
successLabel={labels.success}
/>
</motion.span>
</span>
);
}
const FluxButton = React.forwardRef<HTMLButtonElement, FluxButtonProps>(
(
{
className,
disabled = false,
idleLabel,
loadingLabel,
onAction,
onClick,
onKeyDown,
onPointerDown,
size = "default",
successHold = SUCCESS_HOLD_MS,
successIcon,
successLabel,
type = "button",
variant = "default",
...props
},
ref
) => {
const resetTimeoutRef = React.useRef<number | null>(null);
const widthAnimationRef = React.useRef<ReturnType<typeof animate> | null>(
null
);
const mountedRef = React.useRef(true);
const phaseRef = React.useRef<FlowPhase>("idle");
const actionGenerationRef = React.useRef(0);
const onActionRef = React.useRef(onAction);
const [hasInteracted, setHasInteracted] = React.useState(false);
const [phase, setPhase] = React.useState<FlowPhase>("idle");
const resolvedSize = size ?? "default";
const resolvedVariant = variant ?? "default";
const layout = getLayoutMetrics(resolvedSize, resolvedVariant);
const initialWidth: number = layout.height;
const widthMv = useMotionValue(initialWidth);
const iconSlotClassName = ICON_SLOT_CLASS[resolvedSize];
const busyGapClassName = BUSY_GAP_CLASS[resolvedSize];
const resolvedSuccessHold = Math.max(0, successHold);
phaseRef.current = phase;
onActionRef.current = onAction;
const safeSetPhase = React.useCallback((nextPhase: FlowPhase) => {
if (mountedRef.current) {
setPhase(nextPhase);
}
}, []);
const labels = React.useMemo(
() => ({
idle: idleLabel,
loading: loadingLabel,
success: successLabel,
}),
[idleLabel, loadingLabel, successLabel]
);
const { measureRef, widths } = useMeasuredWidths(
layout.paddingX,
layout.height
);
const animateEnabled = hasInteracted;
const isBusy = phase !== "idle";
const isLocked = Boolean(disabled || isBusy);
const buttonClassName = cn(
fluxButtonVariants({ size, variant }),
isBusy && "pointer-events-none",
className
);
React.useLayoutEffect(() => {
if (phase !== "idle" && hasInteracted) {
return;
}
widthMv.set(widths.idle);
}, [hasInteracted, phase, widthMv, widths.idle]);
React.useEffect(() => {
const target = getWidthTarget(phase, widths);
widthAnimationRef.current?.stop();
const finishShrinking = () => {
if (phaseRef.current === "shrinking") {
safeSetPhase("idle");
}
};
if (Math.abs(widthMv.get() - target) < 0.5) {
widthMv.set(target);
finishShrinking();
return;
}
if (!animateEnabled) {
widthMv.set(target);
finishShrinking();
return;
}
widthAnimationRef.current = animate(widthMv, target, {
...FLUID_WIDTH,
velocity: widthMv.getVelocity(),
});
if (phase !== "shrinking") {
return;
}
let cancelled = false;
widthAnimationRef.current
.then(() => {
if (!cancelled) {
finishShrinking();
}
})
.catch(() => undefined);
return () => {
cancelled = true;
widthAnimationRef.current?.stop();
};
}, [animateEnabled, phase, safeSetPhase, widthMv, widths]);
React.useEffect(() => {
if (phase !== "shrinking") {
return;
}
const fallback = window.setTimeout(() => {
safeSetPhase("idle");
}, SHRINK_FALLBACK_MS);
return () => window.clearTimeout(fallback);
}, [phase, safeSetPhase]);
React.useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current);
}
widthAnimationRef.current?.stop();
};
}, []);
const beginShrink = React.useCallback(() => {
safeSetPhase("shrinking");
}, [safeSetPhase]);
const runAction = React.useCallback(async () => {
if (phaseRef.current !== "idle" || disabled) {
return;
}
const generation = actionGenerationRef.current + 1;
actionGenerationRef.current = generation;
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current);
resetTimeoutRef.current = null;
}
setHasInteracted(true);
safeSetPhase("loading");
try {
await onActionRef.current();
if (!mountedRef.current || actionGenerationRef.current !== generation) {
return;
}
safeSetPhase("success");
resetTimeoutRef.current = window.setTimeout(() => {
resetTimeoutRef.current = null;
if (
!mountedRef.current ||
actionGenerationRef.current !== generation
) {
return;
}
beginShrink();
}, resolvedSuccessHold);
} catch {
if (!mountedRef.current || actionGenerationRef.current !== generation) {
return;
}
beginShrink();
}
}, [beginShrink, disabled, resolvedSuccessHold, safeSetPhase]);
return (
<>
<div
aria-hidden
className={cn(
buttonClassName,
"pointer-events-none invisible absolute -z-10 inline-flex flex-col whitespace-nowrap"
)}
ref={measureRef}
>
<span data-width="idle">{labels.idle}</span>
<span
className={cn("inline-flex items-center", busyGapClassName)}
data-width="loading"
>
<span className={cn("inline-flex shrink-0", iconSlotClassName)} />
<span>{labels.loading}</span>
</span>
<span
className={cn(
"inline-flex items-center",
successIcon && busyGapClassName
)}
data-width="success"
>
{successIcon ? (
<span className={cn("inline-flex shrink-0", iconSlotClassName)} />
) : null}
<span>{labels.success}</span>
</span>
</div>
<ButtonPrimitive
{...props}
aria-busy={phase === "loading" || undefined}
aria-disabled={isLocked || undefined}
aria-live="polite"
className={buttonClassName}
data-slot="button"
disabled={Boolean(disabled)}
onClick={(event) => {
onClick?.(event);
if (event.defaultPrevented || isLocked) {
return;
}
event.preventDefault();
runAction().catch(() => undefined);
}}
onKeyDown={(event) => {
onKeyDown?.(event);
if (
event.defaultPrevented ||
isLocked ||
event.repeat ||
(event.key !== " " && event.key !== "Enter")
) {
return;
}
if (event.key === " ") {
event.preventDefault();
}
runAction().catch(() => undefined);
}}
onPointerDown={onPointerDown}
render={(buttonProps) => {
const {
primitiveClassName,
primitiveRef,
primitiveStyle,
resolvedButtonProps,
} = resolvePrimitiveButtonProps(buttonProps);
return (
<motion.button
{...resolvedButtonProps}
className={primitiveClassName}
ref={(node) => {
setRef(primitiveRef, node);
setRef(ref, node);
}}
style={{
...primitiveStyle,
height: layout.height,
minWidth: layout.height,
width: widthMv,
}}
transition={FLUID_TAP}
type={type}
whileTap={
isLocked || !animateEnabled || variant === "link"
? undefined
: { scale: 0.96 }
}
>
<span className="relative z-10 inline-flex w-full items-center justify-center">
<FlowContent
animateEnabled={hasInteracted}
labels={labels}
phase={phase}
size={resolvedSize}
successIcon={successIcon}
/>
</span>
</motion.button>
);
}}
type={type}
/>
</>
);
}
);
FluxButton.displayName = "FluxButton";
export { FluxButton, fluxButtonVariants };
export type { FluxButtonProps };
Flux Button
Try the interaction, copy the React source files, or download the complete example.
About Flux Button
Flux Button is an animated React and Tailwind CSS button by Edwin Vakayil. Try the interaction, copy the React source, or download the example with creator credit.
- Effects and categories
- Hover effects, Animated
- Using this example
- Copy a TSX file or download the original source and standalone demo. Test keyboard focus, motion and contrast in your own project.
Usage, Tailwind and licensing questions
Read the source files
import-examples/flux-button.tsx
import {FluxButton} from "@/registry/flux-button";export default function Demo(){return <FluxButton idleLabel="Save changes" loadingLabel="Saving" successLabel="Saved" onAction={()=>new Promise(resolve=>setTimeout(resolve,900))}/>}registry/flux-button.tsx
"use client";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import { animate, motion, useMotionValue } from "motion/react";
import * as React from "react";
import { cn } from "@/lib/utils";
const buttonThemeClassName =
"[--button-destructive:#dc2626] [--button-input:#e3e7ec] [--button-primary-foreground:#ffffff] [--button-ring:rgba(17,17,17,0.16)] [--button-ripple:color-mix(in_oklch,var(--foreground),transparent_52%)] [--button-secondary:#eceff3] [--button-secondary-foreground:#111111] dark:[--button-destructive:#f87171] dark:[--button-input:#2b2a25] dark:[--button-primary-foreground:#111111] dark:[--button-ring:rgba(246,243,236,0.18)] dark:[--button-ripple:color-mix(in_oklch,var(--foreground),transparent_30%)] dark:[--button-secondary:#2a2a27] dark:[--button-secondary-foreground:#f6f3ec]";
const buttonCornerClassName =
"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]";
const buttonCornerXsClassName =
"rounded-[min(var(--radius-md),10px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[12px]";
const buttonCornerSmClassName =
"rounded-[min(var(--radius-md),12px)] supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[14px]";
const buttonGroupCornerClassName =
"in-data-[slot=button-group]:rounded-lg in-data-[slot=button-group]:supports-[corner-shape:squircle]:corner-squircle in-data-[slot=button-group]:supports-[corner-shape:squircle]:rounded-[11px]";
const SUCCESS_HOLD_MS = 1000;
const FLUID_WIDTH = {
type: "spring" as const,
stiffness: 210,
damping: 28,
mass: 1.08,
};
const FLUID_CONTENT = {
type: "spring" as const,
stiffness: 340,
damping: 32,
mass: 0.92,
};
const FLUID_ICON = {
type: "spring" as const,
stiffness: 400,
damping: 30,
mass: 0.76,
};
const FLUID_REVEAL = {
type: "spring" as const,
stiffness: 260,
damping: 30,
mass: 0.98,
};
const FLUID_TAP = {
type: "spring" as const,
stiffness: 520,
damping: 34,
mass: 0.72,
};
const NO_TRANSITION = { duration: 0 } as const;
type FlowPhase = "idle" | "loading" | "success" | "shrinking";
function getWidthTarget(
phase: FlowPhase,
widths: { idle: number; loading: number; success: number }
) {
switch (phase) {
case "loading":
return widths.loading;
case "success":
return widths.success;
default:
return widths.idle;
}
}
const SIZE_METRICS = {
xs: { height: 24, paddingX: 8 },
sm: { height: 28, paddingX: 10 },
default: { height: 32, paddingX: 10 },
lg: { height: 40, paddingX: 16 },
} as const;
const LINK_PADDING_X = {
xs: 6,
sm: 8,
default: 8,
lg: 10,
} as const;
const ICON_SLOT_CLASS = {
xs: "size-3",
sm: "size-3.5",
default: "size-4",
lg: "size-4",
} as const;
const BUSY_GAP_CLASS = {
xs: "gap-1",
sm: "gap-1",
default: "gap-1.5",
lg: "gap-1.5",
} as const;
const SHRINK_FALLBACK_MS = 900;
const fluxButtonVariants = cva(
cn(
buttonThemeClassName,
buttonCornerClassName,
"relative inline-flex shrink-0 cursor-pointer touch-manipulation select-none items-center justify-center overflow-hidden whitespace-nowrap border border-transparent bg-clip-padding font-medium text-sm no-underline outline-none [-webkit-tap-highlight-color:transparent] focus-visible:border-[color:var(--button-ring)] focus-visible:ring-3 focus-visible:ring-[color:color-mix(in_oklch,var(--button-ring),transparent_50%)] active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
),
{
variants: {
variant: {
default:
"visited:!text-primary-foreground hover:!text-primary-foreground focus-visible:!text-primary-foreground active:!text-primary-foreground bg-primary text-primary-foreground hover:bg-primary dark:[--button-ripple:color-mix(in_oklch,#111111,transparent_10%)] [&_svg]:text-primary-foreground",
outline:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground border-border bg-background text-foreground hover:bg-accent/60 dark:border-[color:var(--button-input)] dark:bg-[color-mix(in_oklch,var(--button-input),transparent_70%)] dark:hover:bg-[color-mix(in_oklch,var(--button-input),transparent_50%)]",
secondary:
"visited:!text-[color:var(--button-secondary-foreground)] hover:!text-[color:var(--button-secondary-foreground)] focus-visible:!text-[color:var(--button-secondary-foreground)] active:!text-[color:var(--button-secondary-foreground)] bg-[color:var(--button-secondary)] text-[color:var(--button-secondary-foreground)] hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_7%)] dark:hover:bg-[color-mix(in_oklch,var(--button-secondary),var(--foreground)_11%)] [&_svg]:text-[color:var(--button-secondary-foreground)]",
ghost:
"visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground active:!text-foreground text-foreground hover:bg-accent/60 dark:hover:bg-muted/50",
destructive:
"visited:!text-[color:var(--button-destructive)] hover:!text-[color:var(--button-destructive)] focus-visible:!text-[color:var(--button-destructive)] active:!text-[color:var(--button-destructive)] bg-[color-mix(in_oklch,var(--button-destructive),transparent_90%)] text-[color:var(--button-destructive)] hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] focus-visible:border-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:bg-[color-mix(in_oklch,var(--button-destructive),transparent_80%)] dark:focus-visible:ring-[color:color-mix(in_oklch,var(--button-destructive),transparent_60%)] dark:hover:bg-[color-mix(in_oklch,var(--button-destructive),transparent_70%)]",
link: "visited:!text-foreground hover:!text-foreground focus-visible:!text-foreground text-foreground underline-offset-4 shadow-none hover:underline active:translate-y-0",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: cn(
"h-6 gap-1 px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
buttonGroupCornerClassName,
buttonCornerXsClassName
),
sm: cn(
"h-7 gap-1 px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
buttonGroupCornerClassName,
buttonCornerSmClassName
),
lg: "h-10 gap-1.5 px-4 text-base leading-5 has-data-[icon=inline-end]:pr-3.5 has-data-[icon=inline-start]:pl-3.5",
},
},
compoundVariants: [
{
variant: "link",
size: "default",
class: "h-8 px-2 text-sm leading-5",
},
{
variant: "link",
size: "lg",
class: "h-10 px-2.5 text-base leading-5",
},
{
variant: "link",
size: "sm",
class: "h-7 px-2 text-[0.8rem]",
},
{
variant: "link",
size: "xs",
class: "h-6 px-1.5 text-xs",
},
],
defaultVariants: {
size: "default",
variant: "default",
},
}
);
type ButtonHTMLAttributesForMotion = Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
| "onAnimationEnd"
| "onAnimationIteration"
| "onAnimationStart"
| "onDrag"
| "onDragEnd"
| "onDragEnter"
| "onDragExit"
| "onDragLeave"
| "onDragOver"
| "onDragStart"
| "onDrop"
>;
type ButtonRenderProps = React.HTMLAttributes<HTMLButtonElement> & {
className?: string;
ref?: React.Ref<HTMLButtonElement>;
style?: React.CSSProperties;
};
function setRef<T>(ref: React.Ref<T> | undefined, value: T) {
if (typeof ref === "function") {
ref(value);
return;
}
if (ref) {
(ref as React.MutableRefObject<T>).current = value;
}
}
function resolvePrimitiveButtonProps(buttonProps: ButtonRenderProps) {
const {
children: _buttonChildren,
className: primitiveClassName,
onAnimationEnd: _onAnimationEnd,
onAnimationIteration: _onAnimationIteration,
onAnimationStart: _onAnimationStart,
onDrag: _onDrag,
onDragEnd: _onDragEnd,
onDragEnter: _onDragEnter,
onDragExit: _onDragExit,
onDragLeave: _onDragLeave,
onDragOver: _onDragOver,
onDragStart: _onDragStart,
onDrop: _onDrop,
ref: primitiveRef,
style: primitiveStyle,
...resolvedButtonProps
} = buttonProps;
return {
primitiveClassName,
primitiveRef,
primitiveStyle,
resolvedButtonProps,
};
}
type FluxButtonProps = Omit<ButtonHTMLAttributesForMotion, "type"> &
VariantProps<typeof fluxButtonVariants> & {
idleLabel: string;
loadingLabel: string;
successLabel: string;
successIcon?: React.ReactNode;
onAction: () => void | Promise<void>;
successHold?: number;
type?: "button" | "submit";
};
function getLayoutMetrics(
size: NonNullable<FluxButtonProps["size"]>,
variant: NonNullable<FluxButtonProps["variant"]>
) {
const metrics = SIZE_METRICS[size];
return {
height: metrics.height,
paddingX: variant === "link" ? LINK_PADDING_X[size] : metrics.paddingX,
};
}
function useMeasuredWidths(paddingX: number, minWidth: number) {
const measureRef = React.useRef<HTMLDivElement>(null);
const [widths, setWidths] = React.useState({
idle: minWidth,
loading: minWidth,
success: minWidth,
});
React.useLayoutEffect(() => {
const root = measureRef.current;
if (!root) {
return;
}
const readWidth = (selector: string) => {
const node = root.querySelector<HTMLElement>(selector);
return Math.max(minWidth, (node?.offsetWidth ?? 0) + paddingX * 2);
};
const measure = () => {
setWidths({
idle: readWidth('[data-width="idle"]'),
loading: readWidth('[data-width="loading"]'),
success: readWidth('[data-width="success"]'),
});
};
measure();
const resizeObserver = new ResizeObserver(measure);
resizeObserver.observe(root);
return () => resizeObserver.disconnect();
}, [minWidth, paddingX]);
return { measureRef, widths };
}
type FlowContentProps = {
animateEnabled: boolean;
labels: { idle: string; loading: string; success: string };
phase: FlowPhase;
size: NonNullable<FluxButtonProps["size"]>;
successIcon?: React.ReactNode;
};
function useFlowContentMotion(animateEnabled: boolean, phase: FlowPhase) {
const motionEnabled = animateEnabled;
const contentTransition = motionEnabled ? FLUID_CONTENT : NO_TRANSITION;
const iconTransition = motionEnabled ? FLUID_ICON : NO_TRANSITION;
const shrinkTransition = motionEnabled ? FLUID_REVEAL : NO_TRANSITION;
const isShrinking = phase === "shrinking";
return {
contentTransition,
iconTransition,
idleTransition: isShrinking ? shrinkTransition : contentTransition,
busyTransition: isShrinking ? NO_TRANSITION : contentTransition,
};
}
function FlowIdleLayer({
idleLabel,
idleOpacity,
idleTransition,
isIdle,
isShrinking,
}: {
idleLabel: string;
idleOpacity: number;
idleTransition:
| typeof FLUID_CONTENT
| typeof FLUID_REVEAL
| typeof NO_TRANSITION;
isIdle: boolean;
isShrinking: boolean;
}) {
return (
<motion.span
animate={{ opacity: idleOpacity }}
aria-hidden={!(isIdle || isShrinking)}
className="absolute inset-0 inline-flex items-center justify-center whitespace-nowrap"
initial={false}
style={{ pointerEvents: "none", zIndex: idleOpacity > 0.5 ? 1 : 0 }}
transition={idleTransition}
>
{idleLabel}
</motion.span>
);
}
function FlowIconSlot({
iconSlotClassName,
iconTransition,
isLoading,
isSuccess,
showSuccessIcon,
successIcon,
}: {
iconSlotClassName: string;
iconTransition: typeof FLUID_ICON | typeof NO_TRANSITION;
isLoading: boolean;
isSuccess: boolean;
showSuccessIcon: boolean;
successIcon?: React.ReactNode;
}) {
return (
<span
className={cn("relative shrink-0 [&_svg]:size-[1em]", iconSlotClassName)}
>
<motion.span
animate={{
opacity: isLoading ? 1 : 0,
rotate: isLoading ? 0 : 72,
scale: isLoading ? 1 : 0.82,
}}
aria-hidden={!isLoading}
className="absolute inset-0 inline-flex items-center justify-center"
initial={false}
transition={iconTransition}
>
<Loader2
aria-hidden
className={cn(iconSlotClassName, isLoading && "animate-spin")}
/>
</motion.span>
{showSuccessIcon ? (
<motion.span
animate={{
opacity: isSuccess ? 1 : 0,
rotate: isSuccess ? 0 : -72,
scale: isSuccess ? 1 : 0.82,
}}
aria-hidden={!isSuccess}
className="absolute inset-0 inline-flex items-center justify-center"
initial={false}
transition={iconTransition}
>
{successIcon}
</motion.span>
) : null}
</span>
);
}
function FlowBusyLabels({
contentTransition,
isLoading,
isSuccess,
loadingLabel,
successLabel,
}: {
contentTransition: typeof FLUID_CONTENT | typeof NO_TRANSITION;
isLoading: boolean;
isSuccess: boolean;
loadingLabel: string;
successLabel: string;
}) {
return (
<span className="relative inline-grid [grid-template-columns:max-content] [grid-template-rows:1.25em]">
<motion.span
animate={{ opacity: isLoading ? 1 : 0 }}
aria-hidden={!isLoading}
className="col-start-1 row-start-1 inline-flex items-center whitespace-nowrap"
initial={false}
style={{ zIndex: isLoading ? 1 : 0 }}
transition={contentTransition}
>
{loadingLabel}
</motion.span>
<motion.span
animate={{ opacity: isSuccess ? 1 : 0 }}
aria-hidden={!isSuccess}
className="col-start-1 row-start-1 inline-flex items-center whitespace-nowrap"
initial={false}
style={{ zIndex: isSuccess ? 1 : 0 }}
transition={contentTransition}
>
{successLabel}
</motion.span>
</span>
);
}
function FlowContent({
animateEnabled,
labels,
phase,
size,
successIcon,
}: FlowContentProps) {
const { busyTransition, contentTransition, iconTransition, idleTransition } =
useFlowContentMotion(animateEnabled, phase);
const isIdle = phase === "idle";
const isShrinking = phase === "shrinking";
const isLoading = phase === "loading";
const isSuccess = phase === "success";
const isBusy = isLoading || isSuccess;
const showSuccessIcon = Boolean(successIcon);
const showIconSlot = isLoading || (isSuccess && showSuccessIcon);
const iconSlotClassName = ICON_SLOT_CLASS[size];
const busyGapClassName = BUSY_GAP_CLASS[size];
const idleOpacity = isIdle || isShrinking ? 1 : 0;
const busyOpacity = isBusy ? 1 : 0;
return (
<span className="relative h-[1.25em] w-full leading-none">
<FlowIdleLayer
idleLabel={labels.idle}
idleOpacity={idleOpacity}
idleTransition={idleTransition}
isIdle={isIdle}
isShrinking={isShrinking}
/>
<motion.span
animate={{ opacity: busyOpacity }}
aria-hidden={!isBusy}
className={cn(
"absolute inset-0 inline-flex items-center justify-center whitespace-nowrap",
showIconSlot && busyGapClassName
)}
initial={false}
style={{ pointerEvents: "none", zIndex: busyOpacity > 0.5 ? 1 : 0 }}
transition={busyTransition}
>
{showIconSlot ? (
<FlowIconSlot
iconSlotClassName={iconSlotClassName}
iconTransition={iconTransition}
isLoading={isLoading}
isSuccess={isSuccess}
showSuccessIcon={showSuccessIcon}
successIcon={successIcon}
/>
) : null}
<FlowBusyLabels
contentTransition={contentTransition}
isLoading={isLoading}
isSuccess={isSuccess}
loadingLabel={labels.loading}
successLabel={labels.success}
/>
</motion.span>
</span>
);
}
const FluxButton = React.forwardRef<HTMLButtonElement, FluxButtonProps>(
(
{
className,
disabled = false,
idleLabel,
loadingLabel,
onAction,
onClick,
onKeyDown,
onPointerDown,
size = "default",
successHold = SUCCESS_HOLD_MS,
successIcon,
successLabel,
type = "button",
variant = "default",
...props
},
ref
) => {
const resetTimeoutRef = React.useRef<number | null>(null);
const widthAnimationRef = React.useRef<ReturnType<typeof animate> | null>(
null
);
const mountedRef = React.useRef(true);
const phaseRef = React.useRef<FlowPhase>("idle");
const actionGenerationRef = React.useRef(0);
const onActionRef = React.useRef(onAction);
const [hasInteracted, setHasInteracted] = React.useState(false);
const [phase, setPhase] = React.useState<FlowPhase>("idle");
const resolvedSize = size ?? "default";
const resolvedVariant = variant ?? "default";
const layout = getLayoutMetrics(resolvedSize, resolvedVariant);
const initialWidth: number = layout.height;
const widthMv = useMotionValue(initialWidth);
const iconSlotClassName = ICON_SLOT_CLASS[resolvedSize];
const busyGapClassName = BUSY_GAP_CLASS[resolvedSize];
const resolvedSuccessHold = Math.max(0, successHold);
phaseRef.current = phase;
onActionRef.current = onAction;
const safeSetPhase = React.useCallback((nextPhase: FlowPhase) => {
if (mountedRef.current) {
setPhase(nextPhase);
}
}, []);
const labels = React.useMemo(
() => ({
idle: idleLabel,
loading: loadingLabel,
success: successLabel,
}),
[idleLabel, loadingLabel, successLabel]
);
const { measureRef, widths } = useMeasuredWidths(
layout.paddingX,
layout.height
);
const animateEnabled = hasInteracted;
const isBusy = phase !== "idle";
const isLocked = Boolean(disabled || isBusy);
const buttonClassName = cn(
fluxButtonVariants({ size, variant }),
isBusy && "pointer-events-none",
className
);
React.useLayoutEffect(() => {
if (phase !== "idle" && hasInteracted) {
return;
}
widthMv.set(widths.idle);
}, [hasInteracted, phase, widthMv, widths.idle]);
React.useEffect(() => {
const target = getWidthTarget(phase, widths);
widthAnimationRef.current?.stop();
const finishShrinking = () => {
if (phaseRef.current === "shrinking") {
safeSetPhase("idle");
}
};
if (Math.abs(widthMv.get() - target) < 0.5) {
widthMv.set(target);
finishShrinking();
return;
}
if (!animateEnabled) {
widthMv.set(target);
finishShrinking();
return;
}
widthAnimationRef.current = animate(widthMv, target, {
...FLUID_WIDTH,
velocity: widthMv.getVelocity(),
});
if (phase !== "shrinking") {
return;
}
let cancelled = false;
widthAnimationRef.current
.then(() => {
if (!cancelled) {
finishShrinking();
}
})
.catch(() => undefined);
return () => {
cancelled = true;
widthAnimationRef.current?.stop();
};
}, [animateEnabled, phase, safeSetPhase, widthMv, widths]);
React.useEffect(() => {
if (phase !== "shrinking") {
return;
}
const fallback = window.setTimeout(() => {
safeSetPhase("idle");
}, SHRINK_FALLBACK_MS);
return () => window.clearTimeout(fallback);
}, [phase, safeSetPhase]);
React.useEffect(() => {
mountedRef.current = true;
return () => {
mountedRef.current = false;
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current);
}
widthAnimationRef.current?.stop();
};
}, []);
const beginShrink = React.useCallback(() => {
safeSetPhase("shrinking");
}, [safeSetPhase]);
const runAction = React.useCallback(async () => {
if (phaseRef.current !== "idle" || disabled) {
return;
}
const generation = actionGenerationRef.current + 1;
actionGenerationRef.current = generation;
if (resetTimeoutRef.current !== null) {
window.clearTimeout(resetTimeoutRef.current);
resetTimeoutRef.current = null;
}
setHasInteracted(true);
safeSetPhase("loading");
try {
await onActionRef.current();
if (!mountedRef.current || actionGenerationRef.current !== generation) {
return;
}
safeSetPhase("success");
resetTimeoutRef.current = window.setTimeout(() => {
resetTimeoutRef.current = null;
if (
!mountedRef.current ||
actionGenerationRef.current !== generation
) {
return;
}
beginShrink();
}, resolvedSuccessHold);
} catch {
if (!mountedRef.current || actionGenerationRef.current !== generation) {
return;
}
beginShrink();
}
}, [beginShrink, disabled, resolvedSuccessHold, safeSetPhase]);
return (
<>
<div
aria-hidden
className={cn(
buttonClassName,
"pointer-events-none invisible absolute -z-10 inline-flex flex-col whitespace-nowrap"
)}
ref={measureRef}
>
<span data-width="idle">{labels.idle}</span>
<span
className={cn("inline-flex items-center", busyGapClassName)}
data-width="loading"
>
<span className={cn("inline-flex shrink-0", iconSlotClassName)} />
<span>{labels.loading}</span>
</span>
<span
className={cn(
"inline-flex items-center",
successIcon && busyGapClassName
)}
data-width="success"
>
{successIcon ? (
<span className={cn("inline-flex shrink-0", iconSlotClassName)} />
) : null}
<span>{labels.success}</span>
</span>
</div>
<ButtonPrimitive
{...props}
aria-busy={phase === "loading" || undefined}
aria-disabled={isLocked || undefined}
aria-live="polite"
className={buttonClassName}
data-slot="button"
disabled={Boolean(disabled)}
onClick={(event) => {
onClick?.(event);
if (event.defaultPrevented || isLocked) {
return;
}
event.preventDefault();
runAction().catch(() => undefined);
}}
onKeyDown={(event) => {
onKeyDown?.(event);
if (
event.defaultPrevented ||
isLocked ||
event.repeat ||
(event.key !== " " && event.key !== "Enter")
) {
return;
}
if (event.key === " ") {
event.preventDefault();
}
runAction().catch(() => undefined);
}}
onPointerDown={onPointerDown}
render={(buttonProps) => {
const {
primitiveClassName,
primitiveRef,
primitiveStyle,
resolvedButtonProps,
} = resolvePrimitiveButtonProps(buttonProps);
return (
<motion.button
{...resolvedButtonProps}
className={primitiveClassName}
ref={(node) => {
setRef(primitiveRef, node);
setRef(ref, node);
}}
style={{
...primitiveStyle,
height: layout.height,
minWidth: layout.height,
width: widthMv,
}}
transition={FLUID_TAP}
type={type}
whileTap={
isLocked || !animateEnabled || variant === "link"
? undefined
: { scale: 0.96 }
}
>
<span className="relative z-10 inline-flex w-full items-center justify-center">
<FlowContent
animateEnabled={hasInteracted}
labels={labels}
phase={phase}
size={resolvedSize}
successIcon={successIcon}
/>
</span>
</motion.button>
);
}}
type={type}
/>
</>
);
}
);
FluxButton.displayName = "FluxButton";
export { FluxButton, fluxButtonVariants };
export type { FluxButtonProps };
lib/utils.ts
import type { ClassValue } from "clsx";
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
demo.tsx
export { default as default } from "@/import-examples/flux-button";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
"Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
"Segoe UI Symbol", "Noto Color Emoji";
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--spacing: 0.25rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-base: 1rem;
--text-base--line-height: calc(1.5 / 1);
--font-weight-medium: 500;
--radius-md: calc(var(--radius) - 2px);
--animate-spin: spin 1s linear infinite;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-sans);
--default-mono-font-family: var(--font-mono);
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
html, :host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring:where(:not(iframe)) {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
display: block;
vertical-align: middle;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea, ::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
}
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
::-webkit-calendar-picker-indicator {
line-height: 1;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
}
@layer utilities {
.pointer-events-none {
pointer-events: none;
}
.invisible {
visibility: hidden;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.inset-0 {
inset: 0px;
}
.-z-10 {
z-index: calc(10 * -1);
}
.z-10 {
z-index: 10;
}
.col-start-1 {
grid-column-start: 1;
}
.row-start-1 {
grid-row-start: 1;
}
.inline-flex {
display: inline-flex;
}
.inline-grid {
display: inline-grid;
}
.size-3 {
width: calc(var(--spacing) * 3);
height: calc(var(--spacing) * 3);
}
.size-3\.5 {
width: calc(var(--spacing) * 3.5);
height: calc(var(--spacing) * 3.5);
}
.size-4 {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
.h-6 {
height: calc(var(--spacing) * 6);
}
.h-7 {
height: calc(var(--spacing) * 7);
}
.h-8 {
height: calc(var(--spacing) * 8);
}
.h-10 {
height: calc(var(--spacing) * 10);
}
.h-\[1\.25em\] {
height: 1.25em;
}
.w-full {
width: 100%;
}
.shrink-0 {
flex-shrink: 0;
}
.animate-spin {
animation: var(--animate-spin);
}
.cursor-pointer {
cursor: pointer;
}
.touch-manipulation {
touch-action: manipulation;
}
.\[grid-template-columns\:max-content\] {
grid-template-columns: max-content;
}
.\[grid-template-rows\:1\.25em\] {
grid-template-rows: 1.25em;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-1 {
gap: var(--spacing);
}
.gap-1\.5 {
gap: calc(var(--spacing) * 1.5);
}
.overflow-hidden {
overflow: hidden;
}
.rounded-\[min\(var\(--radius-md\)\,10px\)\] {
border-radius: min(var(--radius-md), 10px);
}
.rounded-\[min\(var\(--radius-md\)\,12px\)\] {
border-radius: min(var(--radius-md), 12px);
}
.rounded-lg {
border-radius: var(--radius);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-border {
border-color: var(--border);
}
.border-transparent {
border-color: transparent;
}
.bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_90\%\)\] {
background-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklch,var(--button-destructive),transparent 90%);
}
}
.bg-\[color\:var\(--button-secondary\)\] {
background-color: var(--button-secondary);
}
.bg-background {
background-color: var(--background);
}
.bg-primary {
background-color: var(--primary);
}
.bg-clip-padding {
background-clip: padding-box;
}
.px-1\.5 {
padding-inline: calc(var(--spacing) * 1.5);
}
.px-2 {
padding-inline: calc(var(--spacing) * 2);
}
.px-2\.5 {
padding-inline: calc(var(--spacing) * 2.5);
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.text-base {
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.text-xs {
font-size: var(--text-xs);
line-height: var(--tw-leading, var(--text-xs--line-height));
}
.text-\[0\.8rem\] {
font-size: 0.8rem;
}
.leading-5 {
--tw-leading: calc(var(--spacing) * 5);
line-height: calc(var(--spacing) * 5);
}
.leading-none {
--tw-leading: 1;
line-height: 1;
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-\[color\:var\(--button-destructive\)\] {
color: var(--button-destructive);
}
.text-\[color\:var\(--button-secondary-foreground\)\] {
color: var(--button-secondary-foreground);
}
.text-foreground {
color: var(--foreground);
}
.text-primary-foreground {
color: var(--primary-foreground);
}
.no-underline {
text-decoration-line: none;
}
.underline-offset-4 {
text-underline-offset: 4px;
}
.shadow-none {
--tw-shadow: 0 0 #0000;
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
.\[--button-destructive\:\#dc2626\] {
--button-destructive: #dc2626;
}
.\[--button-input\:\#e3e7ec\] {
--button-input: #e3e7ec;
}
.\[--button-primary-foreground\:\#ffffff\] {
--button-primary-foreground: #ffffff;
}
.\[--button-ring\:rgba\(17\,17\,17\,0\.16\)\] {
--button-ring: rgba(17,17,17,0.16);
}
.\[--button-ripple\:color-mix\(in_oklch\,var\(--foreground\)\,transparent_52\%\)\] {
--button-ripple: var(--foreground);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--foreground),transparent 52%);
}
}
.\[--button-secondary-foreground\:\#111111\] {
--button-secondary-foreground: #111111;
}
.\[--button-secondary\:\#eceff3\] {
--button-secondary: #eceff3;
}
.\[-webkit-tap-highlight-color\:transparent\] {
-webkit-tap-highlight-color: transparent;
}
.visited\:\!text-\[color\:var\(--button-destructive\)\]:visited {
color: var(--button-destructive) !important;
}
.visited\:\!text-\[color\:var\(--button-secondary-foreground\)\]:visited {
color: var(--button-secondary-foreground) !important;
}
.visited\:\!text-foreground:visited {
color: var(--foreground) !important;
}
.visited\:\!text-primary-foreground:visited {
color: var(--primary-foreground) !important;
}
@media (hover: hover) {
.hover\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_80\%\)\]:hover {
background-color: var(--button-destructive);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_80\%\)\]:hover {
background-color: color-mix(in oklch,var(--button-destructive),transparent 80%);
}
}
.hover\:bg-\[color-mix\(in_oklch\,var\(--button-secondary\)\,var\(--foreground\)_7\%\)\]:hover {
background-color: var(--button-secondary);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-\[color-mix\(in_oklch\,var\(--button-secondary\)\,var\(--foreground\)_7\%\)\]:hover {
background-color: color-mix(in oklch,var(--button-secondary),var(--foreground) 7%);
}
}
.hover\:bg-accent\/60:hover {
background-color: var(--accent);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-accent\/60:hover {
background-color: color-mix(in oklab, var(--accent) 60%, transparent);
}
}
.hover\:bg-primary:hover {
background-color: var(--primary);
}
.hover\:\!text-\[color\:var\(--button-destructive\)\]:hover {
color: var(--button-destructive) !important;
}
.hover\:\!text-\[color\:var\(--button-secondary-foreground\)\]:hover {
color: var(--button-secondary-foreground) !important;
}
.hover\:\!text-foreground:hover {
color: var(--foreground) !important;
}
.hover\:\!text-primary-foreground:hover {
color: var(--primary-foreground) !important;
}
.hover\:underline:hover {
text-decoration-line: underline;
}
}
.focus-visible\:border-\[color\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_60\%\)\]:focus-visible {
border-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklch,var(--button-destructive),transparent 60%);
}
}
.focus-visible\:border-\[color\:var\(--button-ring\)\]:focus-visible {
border-color: var(--button-ring);
}
.focus-visible\:\!text-\[color\:var\(--button-destructive\)\]:focus-visible {
color: var(--button-destructive) !important;
}
.focus-visible\:\!text-\[color\:var\(--button-secondary-foreground\)\]:focus-visible {
color: var(--button-secondary-foreground) !important;
}
.focus-visible\:\!text-foreground:focus-visible {
color: var(--foreground) !important;
}
.focus-visible\:\!text-primary-foreground:focus-visible {
color: var(--primary-foreground) !important;
}
.focus-visible\:ring-3:focus-visible {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.focus-visible\:ring-\[color\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_80\%\)\]:focus-visible {
--tw-ring-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklch,var(--button-destructive),transparent 80%);
}
}
.focus-visible\:ring-\[color\:color-mix\(in_oklch\,var\(--button-ring\)\,transparent_50\%\)\]:focus-visible {
--tw-ring-color: var(--button-ring);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklch,var(--button-ring),transparent 50%);
}
}
.active\:translate-y-0:active {
--tw-translate-y: 0px;
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.active\:\!text-\[color\:var\(--button-destructive\)\]:active {
color: var(--button-destructive) !important;
}
.active\:\!text-\[color\:var\(--button-secondary-foreground\)\]:active {
color: var(--button-secondary-foreground) !important;
}
.active\:\!text-foreground:active {
color: var(--foreground) !important;
}
.active\:\!text-primary-foreground:active {
color: var(--primary-foreground) !important;
}
.active\:not-aria-\[haspopup\]\:translate-y-px:active:not([aria-haspopup]) {
--tw-translate-y: 1px;
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.disabled\:pointer-events-none:disabled {
pointer-events: none;
}
.disabled\:opacity-50:disabled {
opacity: 50%;
}
:where([data-slot="button-group"]) .in-data-\[slot\=button-group\]\:rounded-lg {
border-radius: var(--radius);
}
.has-data-\[icon\=inline-end\]\:pr-1\.5:has([data-icon="inline-end"]) {
padding-right: calc(var(--spacing) * 1.5);
}
.has-data-\[icon\=inline-end\]\:pr-2:has([data-icon="inline-end"]) {
padding-right: calc(var(--spacing) * 2);
}
.has-data-\[icon\=inline-end\]\:pr-3\.5:has([data-icon="inline-end"]) {
padding-right: calc(var(--spacing) * 3.5);
}
.has-data-\[icon\=inline-start\]\:pl-1\.5:has([data-icon="inline-start"]) {
padding-left: calc(var(--spacing) * 1.5);
}
.has-data-\[icon\=inline-start\]\:pl-2:has([data-icon="inline-start"]) {
padding-left: calc(var(--spacing) * 2);
}
.has-data-\[icon\=inline-start\]\:pl-3\.5:has([data-icon="inline-start"]) {
padding-left: calc(var(--spacing) * 3.5);
}
@supports (corner-shape:squircle) {
.supports-\[corner-shape\:squircle\]\:rounded-\[11px\] {
border-radius: 11px;
}
.supports-\[corner-shape\:squircle\]\:rounded-\[12px\] {
border-radius: 12px;
}
.supports-\[corner-shape\:squircle\]\:rounded-\[14px\] {
border-radius: 14px;
}
:where([data-slot="button-group"]) .in-data-\[slot\=button-group\]\:supports-\[corner-shape\:squircle\]\:rounded-\[11px\] {
border-radius: 11px;
}
}
.dark\:border-\[color\:var\(--button-input\)\]:where(.dark, .dark *) {
border-color: var(--button-input);
}
.dark\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_80\%\)\]:where(.dark, .dark *) {
background-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklch,var(--button-destructive),transparent 80%);
}
}
.dark\:bg-\[color-mix\(in_oklch\,var\(--button-input\)\,transparent_70\%\)\]:where(.dark, .dark *) {
background-color: var(--button-input);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklch,var(--button-input),transparent 70%);
}
}
.dark\:\[--button-destructive\:\#f87171\]:where(.dark, .dark *) {
--button-destructive: #f87171;
}
.dark\:\[--button-input\:\#2b2a25\]:where(.dark, .dark *) {
--button-input: #2b2a25;
}
.dark\:\[--button-primary-foreground\:\#111111\]:where(.dark, .dark *) {
--button-primary-foreground: #111111;
}
.dark\:\[--button-ring\:rgba\(246\,243\,236\,0\.18\)\]:where(.dark, .dark *) {
--button-ring: rgba(246,243,236,0.18);
}
.dark\:\[--button-ripple\:color-mix\(in_oklch\,\#111111\,transparent_10\%\)\]:where(.dark, .dark *) {
--button-ripple: color-mix(in oklch,#111111,transparent 10%);
}
.dark\:\[--button-ripple\:color-mix\(in_oklch\,var\(--foreground\)\,transparent_30\%\)\]:where(.dark, .dark *) {
--button-ripple: var(--foreground);
@supports (color: color-mix(in lab, red, red)) {
--button-ripple: color-mix(in oklch,var(--foreground),transparent 30%);
}
}
.dark\:\[--button-secondary-foreground\:\#f6f3ec\]:where(.dark, .dark *) {
--button-secondary-foreground: #f6f3ec;
}
.dark\:\[--button-secondary\:\#2a2a27\]:where(.dark, .dark *) {
--button-secondary: #2a2a27;
}
@media (hover: hover) {
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_70\%\)\]:where(.dark, .dark *):hover {
background-color: var(--button-destructive);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_70\%\)\]:where(.dark, .dark *):hover {
background-color: color-mix(in oklch,var(--button-destructive),transparent 70%);
}
}
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-input\)\,transparent_50\%\)\]:where(.dark, .dark *):hover {
background-color: var(--button-input);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-input\)\,transparent_50\%\)\]:where(.dark, .dark *):hover {
background-color: color-mix(in oklch,var(--button-input),transparent 50%);
}
}
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-secondary\)\,var\(--foreground\)_11\%\)\]:where(.dark, .dark *):hover {
background-color: var(--button-secondary);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-\[color-mix\(in_oklch\,var\(--button-secondary\)\,var\(--foreground\)_11\%\)\]:where(.dark, .dark *):hover {
background-color: color-mix(in oklch,var(--button-secondary),var(--foreground) 11%);
}
}
.dark\:hover\:bg-muted\/50:where(.dark, .dark *):hover {
background-color: var(--muted);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-muted\/50:where(.dark, .dark *):hover {
background-color: color-mix(in oklab, var(--muted) 50%, transparent);
}
}
}
.dark\:focus-visible\:ring-\[color\:color-mix\(in_oklch\,var\(--button-destructive\)\,transparent_60\%\)\]:where(.dark, .dark *):focus-visible {
--tw-ring-color: var(--button-destructive);
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklch,var(--button-destructive),transparent 60%);
}
}
.\[\&_svg\]\:pointer-events-none svg {
pointer-events: none;
}
.\[\&_svg\]\:size-\[1em\] svg {
width: 1em;
height: 1em;
}
.\[\&_svg\]\:shrink-0 svg {
flex-shrink: 0;
}
.\[\&_svg\]\:text-\[color\:var\(--button-secondary-foreground\)\] svg {
color: var(--button-secondary-foreground);
}
.\[\&_svg\]\:text-primary-foreground svg {
color: var(--primary-foreground);
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3 svg:not([class*='size-']) {
width: calc(var(--spacing) * 3);
height: calc(var(--spacing) * 3);
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-3\.5 svg:not([class*='size-']) {
width: calc(var(--spacing) * 3.5);
height: calc(var(--spacing) * 3.5);
}
.\[\&_svg\:not\(\[class\*\=\'size-\'\]\)\]\:size-4 svg:not([class*='size-']) {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
}
:root {
--background: #fff;
--foreground: #111;
--primary: #171717;
--primary-foreground: #fff;
--secondary: #eee;
--secondary-foreground: #171717;
--muted: #eee;
--muted-foreground: #666;
--accent: #eee;
--accent-foreground: #171717;
--border: #ddd;
--input: #ddd;
--ring: #777;
--destructive: #dc2626;
--destructive-foreground: #fff;
--popover: #fff;
--popover-foreground: #111;
--success: #15803d;
--success-foreground: #fff;
--radius: .625rem;
--hairline: 1px;
}
.dark {
--background: #090909;
--foreground: #fafafa;
--primary: #fafafa;
--primary-foreground: #171717;
--secondary: #262626;
--secondary-foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a3a3a3;
--accent: #262626;
--accent-foreground: #fafafa;
--border: #333;
--input: #333;
--popover: #171717;
--popover-foreground: #fafafa;
}
:root {
--header-height-mobile: 3.5rem;
--header-height-desktop: 3.5rem;
--nav-stack-height-mobile: var(--header-height-mobile);
--nav-stack-height-desktop: var(--header-height-desktop);
--background: #ffffff;
--foreground: #111111;
--primary: #111111;
--primary-foreground: #ffffff;
--secondary: #646b75;
--secondary-foreground: #ffffff;
--surface-border: #e9edf2;
--border: #e3e7ec;
--card: #ffffff;
--card-foreground: #111111;
--muted: #f5f7fa;
--muted-foreground: #6d7480;
--accent: #f3f5f8;
--accent-foreground: #111111;
--input: #e3e7ec;
--ring: rgba(17, 17, 17, 0.16);
--destructive: #dc2626;
--accent-pro: #0ea5e9;
--sidebar-accent: #f5f7fa;
--sidebar-accent-foreground: #111111;
--separator-dash: rgb(227 231 236 / 1);
--selection-bg: #cfeeff;
--selection-fg: #0f172a;
--nav-menu-surface: #f4f4f5;
--nav-menu-item-hover: #e4e4e7;
--shadow-card: inset 0 1px 0 rgb(255 255 255 / 88%), inset 0 -1px 0 rgb(0 0 0 / 5%), 0 1px
1px rgb(0 0 0 / 3%), 0 8px 24px rgb(0 0 0 / 4%);
--radius: 0.4rem;
}
.dark {
--background: #111111;
--foreground: #f6f3ec;
--secondary-bg: #171716;
--secondary: #cbc6bb;
--secondary-foreground: #111111;
--primary: #f6f3ec;
--primary-foreground: #111111;
--surface-border: #2a2a25;
--border: #2b2a25;
--card: #111111;
--card-foreground: #f6f3ec;
--muted: #171716;
--muted-foreground: #9a958a;
--accent: #1a1a18;
--accent-foreground: #f6f3ec;
--input: #2b2a25;
--ring: rgba(246, 243, 236, 0.18);
--destructive: #f87171;
--accent-pro: #38bdf8;
--sidebar-accent: #262626;
--sidebar-accent-foreground: #fff;
--separator-dash: rgb(92 91 84 / 1);
--selection-bg: #cfeeff;
--selection-fg: #0f172a;
--nav-menu-surface: #1a1a1c;
--nav-menu-item-hover: #262628;
}
.dark .hero-brush-underline::after {
background: #16a34a;
}
@keyframes status-blink {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.28;
transform: scale(0.82);
}
}
@keyframes iconiq-accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes iconiq-accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
.dark main .text-neutral-500 {
color: rgb(163 163 163);
}
.dark main .text-neutral-600 {
color: rgb(212 212 212);
}
.dark main .text-neutral-700 {
color: rgb(230 230 230);
}
.dark main .text-neutral-800 {
color: rgb(245 245 245);
}
.dark main .text-neutral-900 {
color: rgb(255 255 255);
}
.dark main .text-neutral-400 {
color: rgb(163 163 163);
}
.dark main code {
color: rgb(212 212 212);
}
.dark main code.rounded, .dark main code.rounded-md, .dark main code[class*="bg-neutral-200"], .dark main code[class*="bg-neutral-50"] {
background-color: rgb(38 38 38);
color: rgb(230 230 230);
}
.dark code.rounded, .dark code.rounded-md, .dark code[class*="bg-neutral-200"] {
background-color: rgb(38 38 38);
color: rgb(230 230 230);
}
.dark code[class*="bg-amber-200"] {
background-color: rgb(69 69 40);
color: rgb(253 253 230);
}
.dark main a.text-neutral-700 {
color: rgb(230 230 230);
}
.dark main a:hover {
color: rgb(255 255 255);
}
.dark html[data-route-split-docs] [data-code-block] .shiki, .dark html[data-route-split-docs] [data-code-block] .shiki span {
color: var(--shiki-dark) !important;
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-leading {
syntax: "*";
inherits: false;
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
}
@property --tw-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-inset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
syntax: "*";
inherits: false;
}
@property --tw-ring-offset-width {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --tw-ring-offset-color {
syntax: "*";
inherits: false;
initial-value: #fff;
}
@property --tw-ring-offset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-translate-x {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-y {
syntax: "*";
inherits: false;
initial-value: 0;
}
@property --tw-translate-z {
syntax: "*";
inherits: false;
initial-value: 0;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-border-style: solid;
--tw-leading: initial;
--tw-font-weight: initial;
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-shadow-alpha: 100%;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-inset-shadow-alpha: 100%;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-outline-style: solid;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-translate-z: 0;
}
}
}