"use client";
/**
* @author: @dorianbaffier
* @description: Hold Button
* @version: 2.0.0
* @date: 2026-10-16
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*
* Press and hold to confirm. The fill tracks the hold, drains back at a speed
* that matches how far it got when released early, and on completion the
* button turns solid and the label rolls to a drawn check.
*/
import { cva, type VariantProps } from "class-variance-authority";
import {ArchiveIcon,BanIcon,LogOutIcon,type LucideIcon,SendIcon,Trash2Icon} from "lucide-react";
import {
AnimatePresence,
type AnimationPlaybackControls,
animate,
MotionConfig,
motion,
useMotionValue,
useReducedMotion,
useTransform,
} from "motion/react";
import {
type ButtonHTMLAttributes,
type KeyboardEvent,
type PointerEvent,
type ReactNode,
useEffect,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
const holdButtonVariants = cva(
[
"group/hold relative inline-flex h-10 min-w-44 touch-none select-none items-center justify-center overflow-hidden rounded-lg px-4 font-medium text-sm",
"border [-webkit-touch-callout:none]",
"transition-[background-color,border-color,color] duration-150 ease-out",
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-current/40 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-950",
"disabled:pointer-events-none disabled:opacity-50",
],
{
variants: {
variant: {
red: [
"border-red-200 bg-red-50 text-red-700 hover:bg-red-100/70",
"dark:border-red-500/25 dark:bg-red-500/10 dark:text-red-300 dark:hover:bg-red-500/15",
"data-complete:border-red-600 data-complete:bg-red-600 data-complete:text-white",
"dark:data-complete:border-red-500 dark:data-complete:bg-red-500 dark:data-complete:text-white",
],
green: [
"border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100/70",
"dark:border-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-300 dark:hover:bg-emerald-500/15",
"data-complete:border-emerald-600 data-complete:bg-emerald-600 data-complete:text-white",
"dark:data-complete:border-emerald-500 dark:data-complete:bg-emerald-500 dark:data-complete:text-white",
],
blue: [
"border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100/70",
"dark:border-blue-500/25 dark:bg-blue-500/10 dark:text-blue-300 dark:hover:bg-blue-500/15",
"data-complete:border-blue-600 data-complete:bg-blue-600 data-complete:text-white",
"dark:data-complete:border-blue-500 dark:data-complete:bg-blue-500 dark:data-complete:text-white",
],
orange: [
"border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100/70",
"dark:border-orange-500/25 dark:bg-orange-500/10 dark:text-orange-300 dark:hover:bg-orange-500/15",
"data-complete:border-orange-600 data-complete:bg-orange-600 data-complete:text-white",
"dark:data-complete:border-orange-500 dark:data-complete:bg-orange-500 dark:data-complete:text-white",
],
grey: [
"border-zinc-200 bg-zinc-50 text-zinc-700 hover:bg-zinc-100/70",
"dark:border-white/10 dark:bg-white/5 dark:text-zinc-300 dark:hover:bg-white/8",
"data-complete:border-zinc-900 data-complete:bg-zinc-900 data-complete:text-white",
"dark:data-complete:border-zinc-100 dark:data-complete:bg-zinc-100 dark:data-complete:text-zinc-900",
],
},
},
defaultVariants: {
variant: "red",
},
}
);
type Variant = NonNullable<VariantProps<typeof holdButtonVariants>["variant"]>;
const VARIANT_DEFAULTS: Record<
Variant,
{ icon: LucideIcon; label: string; completeLabel: string }
> = {
red: { icon: Trash2Icon, label: "Hold to delete", completeLabel: "Deleted" },
green: {
icon: ArchiveIcon,
label: "Hold to archive",
completeLabel: "Archived",
},
blue: { icon: SendIcon, label: "Hold to send", completeLabel: "Sent" },
orange: {
icon: LogOutIcon,
label: "Hold to sign out",
completeLabel: "Signed out",
},
grey: { icon: BanIcon, label: "Hold to block", completeLabel: "Blocked" },
};
interface HoldButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children">,
VariantProps<typeof holdButtonVariants> {
/** Idle label. Defaults to a verb that matches the variant. */
children?: ReactNode;
/** How long (ms) the button has to be held. */
holdDuration?: number;
/** Label while holding. */
holdingLabel?: ReactNode;
/** Label once the hold completes. */
completeLabel?: ReactNode;
/** How long (ms) the complete state shows before resetting. 0 keeps it. */
resetAfter?: number;
/**
* Called once the button has been held for the full `holdDuration`.
* A short click or an early release never calls it.
*/
onHoldComplete?: () => void;
}
type Phase = "idle" | "holding" | "complete";
const HOLD_KEYS = new Set([" ", "Enter"]);
const EASE_OUT = [0.23, 1, 0.32, 1] as const;
const LABEL_TRANSITION = { duration: 0.2, ease: EASE_OUT };
/** Time (ms) a full fill takes to drain after an early release. */
const DRAIN_MS = 300;
/** The holding label waits this long (ms), so a quick tap doesn't flicker it. */
const LABEL_DELAY_MS = 150;
function DrawnCheck() {
return (
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 16 16">
<motion.path
animate={{ pathLength: 1 }}
d="M3.5 8.5 6.5 11.5 12.5 4.5"
initial={{ pathLength: 0 }}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
transition={{ duration: 0.3, delay: 0.1, ease: EASE_OUT }}
/>
</svg>
);
}
export default function HoldButton({
className,
variant = "red",
holdDuration = 1200,
holdingLabel = "Keep holding",
completeLabel,
resetAfter = 1800,
onHoldComplete,
children,
type = "button",
disabled,
onPointerDown,
onPointerUp,
onPointerLeave,
onPointerCancel,
onKeyDown,
onKeyUp,
onBlur,
...props
}: HoldButtonProps) {
const [phase, setPhase] = useState<Phase>("idle");
const [showHoldingLabel, setShowHoldingLabel] = useState(false);
const labelTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const phaseRef = useRef<Phase>("idle");
const progress = useMotionValue(0);
const animationRef = useRef<AnimationPlaybackControls | null>(null);
const resetRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const onHoldCompleteRef = useRef(onHoldComplete);
const shouldReduceMotion = useReducedMotion();
const clipPath = useTransform(
progress,
(p) => `inset(0 ${(1 - p) * 100}% 0 0)`
);
const defaults = VARIANT_DEFAULTS[variant ?? "red"];
const Icon = defaults.icon;
useEffect(() => {
onHoldCompleteRef.current = onHoldComplete;
}, [onHoldComplete]);
useEffect(
() => () => {
animationRef.current?.stop();
if (labelTimerRef.current) {
clearTimeout(labelTimerRef.current);
}
if (resetRef.current) {
clearTimeout(resetRef.current);
}
},
[]
);
const updatePhase = (next: Phase) => {
phaseRef.current = next;
setPhase(next);
if (labelTimerRef.current) {
clearTimeout(labelTimerRef.current);
labelTimerRef.current = null;
}
if (next === "holding") {
labelTimerRef.current = setTimeout(
() => setShowHoldingLabel(true),
LABEL_DELAY_MS
);
} else {
setShowHoldingLabel(false);
}
};
const complete = () => {
updatePhase("complete");
onHoldCompleteRef.current?.();
if (resetAfter > 0) {
resetRef.current = setTimeout(() => {
resetRef.current = null;
progress.set(0);
updatePhase("idle");
}, resetAfter);
}
};
const startHold = () => {
if (phaseRef.current !== "idle" || disabled) {
return;
}
updatePhase("holding");
animationRef.current?.stop();
// Picks up from wherever a previous drain left the fill.
const remaining = (1 - progress.get()) * holdDuration;
animationRef.current = animate(progress, 1, {
duration: remaining / 1000,
ease: "linear",
onComplete: () => {
if (phaseRef.current === "holding") {
complete();
}
},
});
};
const endHold = () => {
if (phaseRef.current !== "holding") {
return;
}
updatePhase("idle");
animationRef.current?.stop();
// A near miss visibly unwinds; a short tap barely moves.
animationRef.current = animate(progress, 0, {
duration: shouldReduceMotion ? 0 : (progress.get() * DRAIN_MS) / 1000,
ease: EASE_OUT,
});
};
const handlePointerDown = (e: PointerEvent<HTMLButtonElement>) => {
onPointerDown?.(e);
if (e.button !== 0) {
return;
}
// Touch pointers are captured implicitly, which would keep the hold
// going after the finger slides off. Release so pointerleave cancels it.
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget.releasePointerCapture(e.pointerId);
}
startHold();
};
const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
onKeyDown?.(e);
if (HOLD_KEYS.has(e.key)) {
// Stops Enter from firing click and Space from scrolling.
e.preventDefault();
if (!e.repeat) {
startHold();
}
}
};
const handleKeyUp = (e: KeyboardEvent<HTMLButtonElement>) => {
onKeyUp?.(e);
if (HOLD_KEYS.has(e.key)) {
endHold();
}
};
let label: ReactNode = children ?? defaults.label;
if (phase === "holding" && showHoldingLabel) {
label = holdingLabel;
} else if (phase === "complete") {
label = completeLabel ?? defaults.completeLabel;
}
const labelKey = phase === "holding" && !showHoldingLabel ? "idle" : phase;
return (
<MotionConfig reducedMotion="user">
<button
{...props}
aria-disabled={phase === "complete" || undefined}
className={cn(holdButtonVariants({ variant }), className)}
data-complete={phase === "complete" || undefined}
data-holding={phase === "holding" || undefined}
disabled={disabled}
onBlur={(e) => {
onBlur?.(e);
endHold();
}}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onPointerCancel={(e) => {
onPointerCancel?.(e);
endHold();
}}
onPointerDown={handlePointerDown}
onPointerLeave={(e) => {
onPointerLeave?.(e);
endHold();
}}
onPointerUp={(e) => {
onPointerUp?.(e);
endHold();
}}
type={type}
>
{/* The fill is the button's ink at low strength, revealed left to
right. It hides once the whole button turns solid. */}
<motion.span
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-current/15 transition-opacity duration-150 group-data-complete/hold:opacity-0 dark:bg-current/20"
style={{ clipPath }}
/>
<span className="relative flex items-center justify-center overflow-hidden">
<AnimatePresence initial={false} mode="popLayout">
<motion.span
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
className="inline-flex items-center gap-2 whitespace-nowrap"
exit={{ opacity: 0, y: -10, filter: "blur(2px)" }}
initial={{ opacity: 0, y: 10, filter: "blur(2px)" }}
key={labelKey}
transition={LABEL_TRANSITION}
>
{labelKey === "complete" ? (
<DrawnCheck />
) : (
<Icon aria-hidden="true" className="size-4" />
)}
{label}
</motion.span>
</AnimatePresence>
</span>
<span aria-live="polite" className="sr-only">
{phase === "complete"
? (completeLabel ?? defaults.completeLabel)
: ""}
</span>
</button>
</MotionConfig>
);
}
Hold Button
By Kokonut UI
Try the interaction, copy the React source files, or download the complete example.
About Hold Button
Hold Button is an animated React and Tailwind CSS button by Kokonut UI. 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/hold-button.tsx
import HoldButton from "@/components/kokonutui/hold-button";
export default function ImportedDemo(){return <>
<div className="flex flex-col items-center gap-3">
<HoldButton />
<HoldButton variant="green" />
<HoldButton variant="grey" holdDuration={2000} />
</div>
</>}components/kokonutui/hold-button.tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Hold Button
* @version: 2.0.0
* @date: 2026-10-16
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*
* Press and hold to confirm. The fill tracks the hold, drains back at a speed
* that matches how far it got when released early, and on completion the
* button turns solid and the label rolls to a drawn check.
*/
import { cva, type VariantProps } from "class-variance-authority";
import {ArchiveIcon,BanIcon,LogOutIcon,type LucideIcon,SendIcon,Trash2Icon} from "lucide-react";
import {
AnimatePresence,
type AnimationPlaybackControls,
animate,
MotionConfig,
motion,
useMotionValue,
useReducedMotion,
useTransform,
} from "motion/react";
import {
type ButtonHTMLAttributes,
type KeyboardEvent,
type PointerEvent,
type ReactNode,
useEffect,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
const holdButtonVariants = cva(
[
"group/hold relative inline-flex h-10 min-w-44 touch-none select-none items-center justify-center overflow-hidden rounded-lg px-4 font-medium text-sm",
"border [-webkit-touch-callout:none]",
"transition-[background-color,border-color,color] duration-150 ease-out",
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-current/40 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-950",
"disabled:pointer-events-none disabled:opacity-50",
],
{
variants: {
variant: {
red: [
"border-red-200 bg-red-50 text-red-700 hover:bg-red-100/70",
"dark:border-red-500/25 dark:bg-red-500/10 dark:text-red-300 dark:hover:bg-red-500/15",
"data-complete:border-red-600 data-complete:bg-red-600 data-complete:text-white",
"dark:data-complete:border-red-500 dark:data-complete:bg-red-500 dark:data-complete:text-white",
],
green: [
"border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100/70",
"dark:border-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-300 dark:hover:bg-emerald-500/15",
"data-complete:border-emerald-600 data-complete:bg-emerald-600 data-complete:text-white",
"dark:data-complete:border-emerald-500 dark:data-complete:bg-emerald-500 dark:data-complete:text-white",
],
blue: [
"border-blue-200 bg-blue-50 text-blue-700 hover:bg-blue-100/70",
"dark:border-blue-500/25 dark:bg-blue-500/10 dark:text-blue-300 dark:hover:bg-blue-500/15",
"data-complete:border-blue-600 data-complete:bg-blue-600 data-complete:text-white",
"dark:data-complete:border-blue-500 dark:data-complete:bg-blue-500 dark:data-complete:text-white",
],
orange: [
"border-orange-200 bg-orange-50 text-orange-700 hover:bg-orange-100/70",
"dark:border-orange-500/25 dark:bg-orange-500/10 dark:text-orange-300 dark:hover:bg-orange-500/15",
"data-complete:border-orange-600 data-complete:bg-orange-600 data-complete:text-white",
"dark:data-complete:border-orange-500 dark:data-complete:bg-orange-500 dark:data-complete:text-white",
],
grey: [
"border-zinc-200 bg-zinc-50 text-zinc-700 hover:bg-zinc-100/70",
"dark:border-white/10 dark:bg-white/5 dark:text-zinc-300 dark:hover:bg-white/8",
"data-complete:border-zinc-900 data-complete:bg-zinc-900 data-complete:text-white",
"dark:data-complete:border-zinc-100 dark:data-complete:bg-zinc-100 dark:data-complete:text-zinc-900",
],
},
},
defaultVariants: {
variant: "red",
},
}
);
type Variant = NonNullable<VariantProps<typeof holdButtonVariants>["variant"]>;
const VARIANT_DEFAULTS: Record<
Variant,
{ icon: LucideIcon; label: string; completeLabel: string }
> = {
red: { icon: Trash2Icon, label: "Hold to delete", completeLabel: "Deleted" },
green: {
icon: ArchiveIcon,
label: "Hold to archive",
completeLabel: "Archived",
},
blue: { icon: SendIcon, label: "Hold to send", completeLabel: "Sent" },
orange: {
icon: LogOutIcon,
label: "Hold to sign out",
completeLabel: "Signed out",
},
grey: { icon: BanIcon, label: "Hold to block", completeLabel: "Blocked" },
};
interface HoldButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children">,
VariantProps<typeof holdButtonVariants> {
/** Idle label. Defaults to a verb that matches the variant. */
children?: ReactNode;
/** How long (ms) the button has to be held. */
holdDuration?: number;
/** Label while holding. */
holdingLabel?: ReactNode;
/** Label once the hold completes. */
completeLabel?: ReactNode;
/** How long (ms) the complete state shows before resetting. 0 keeps it. */
resetAfter?: number;
/**
* Called once the button has been held for the full `holdDuration`.
* A short click or an early release never calls it.
*/
onHoldComplete?: () => void;
}
type Phase = "idle" | "holding" | "complete";
const HOLD_KEYS = new Set([" ", "Enter"]);
const EASE_OUT = [0.23, 1, 0.32, 1] as const;
const LABEL_TRANSITION = { duration: 0.2, ease: EASE_OUT };
/** Time (ms) a full fill takes to drain after an early release. */
const DRAIN_MS = 300;
/** The holding label waits this long (ms), so a quick tap doesn't flicker it. */
const LABEL_DELAY_MS = 150;
function DrawnCheck() {
return (
<svg aria-hidden="true" className="size-4" fill="none" viewBox="0 0 16 16">
<motion.path
animate={{ pathLength: 1 }}
d="M3.5 8.5 6.5 11.5 12.5 4.5"
initial={{ pathLength: 0 }}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
transition={{ duration: 0.3, delay: 0.1, ease: EASE_OUT }}
/>
</svg>
);
}
export default function HoldButton({
className,
variant = "red",
holdDuration = 1200,
holdingLabel = "Keep holding",
completeLabel,
resetAfter = 1800,
onHoldComplete,
children,
type = "button",
disabled,
onPointerDown,
onPointerUp,
onPointerLeave,
onPointerCancel,
onKeyDown,
onKeyUp,
onBlur,
...props
}: HoldButtonProps) {
const [phase, setPhase] = useState<Phase>("idle");
const [showHoldingLabel, setShowHoldingLabel] = useState(false);
const labelTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const phaseRef = useRef<Phase>("idle");
const progress = useMotionValue(0);
const animationRef = useRef<AnimationPlaybackControls | null>(null);
const resetRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const onHoldCompleteRef = useRef(onHoldComplete);
const shouldReduceMotion = useReducedMotion();
const clipPath = useTransform(
progress,
(p) => `inset(0 ${(1 - p) * 100}% 0 0)`
);
const defaults = VARIANT_DEFAULTS[variant ?? "red"];
const Icon = defaults.icon;
useEffect(() => {
onHoldCompleteRef.current = onHoldComplete;
}, [onHoldComplete]);
useEffect(
() => () => {
animationRef.current?.stop();
if (labelTimerRef.current) {
clearTimeout(labelTimerRef.current);
}
if (resetRef.current) {
clearTimeout(resetRef.current);
}
},
[]
);
const updatePhase = (next: Phase) => {
phaseRef.current = next;
setPhase(next);
if (labelTimerRef.current) {
clearTimeout(labelTimerRef.current);
labelTimerRef.current = null;
}
if (next === "holding") {
labelTimerRef.current = setTimeout(
() => setShowHoldingLabel(true),
LABEL_DELAY_MS
);
} else {
setShowHoldingLabel(false);
}
};
const complete = () => {
updatePhase("complete");
onHoldCompleteRef.current?.();
if (resetAfter > 0) {
resetRef.current = setTimeout(() => {
resetRef.current = null;
progress.set(0);
updatePhase("idle");
}, resetAfter);
}
};
const startHold = () => {
if (phaseRef.current !== "idle" || disabled) {
return;
}
updatePhase("holding");
animationRef.current?.stop();
// Picks up from wherever a previous drain left the fill.
const remaining = (1 - progress.get()) * holdDuration;
animationRef.current = animate(progress, 1, {
duration: remaining / 1000,
ease: "linear",
onComplete: () => {
if (phaseRef.current === "holding") {
complete();
}
},
});
};
const endHold = () => {
if (phaseRef.current !== "holding") {
return;
}
updatePhase("idle");
animationRef.current?.stop();
// A near miss visibly unwinds; a short tap barely moves.
animationRef.current = animate(progress, 0, {
duration: shouldReduceMotion ? 0 : (progress.get() * DRAIN_MS) / 1000,
ease: EASE_OUT,
});
};
const handlePointerDown = (e: PointerEvent<HTMLButtonElement>) => {
onPointerDown?.(e);
if (e.button !== 0) {
return;
}
// Touch pointers are captured implicitly, which would keep the hold
// going after the finger slides off. Release so pointerleave cancels it.
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget.releasePointerCapture(e.pointerId);
}
startHold();
};
const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
onKeyDown?.(e);
if (HOLD_KEYS.has(e.key)) {
// Stops Enter from firing click and Space from scrolling.
e.preventDefault();
if (!e.repeat) {
startHold();
}
}
};
const handleKeyUp = (e: KeyboardEvent<HTMLButtonElement>) => {
onKeyUp?.(e);
if (HOLD_KEYS.has(e.key)) {
endHold();
}
};
let label: ReactNode = children ?? defaults.label;
if (phase === "holding" && showHoldingLabel) {
label = holdingLabel;
} else if (phase === "complete") {
label = completeLabel ?? defaults.completeLabel;
}
const labelKey = phase === "holding" && !showHoldingLabel ? "idle" : phase;
return (
<MotionConfig reducedMotion="user">
<button
{...props}
aria-disabled={phase === "complete" || undefined}
className={cn(holdButtonVariants({ variant }), className)}
data-complete={phase === "complete" || undefined}
data-holding={phase === "holding" || undefined}
disabled={disabled}
onBlur={(e) => {
onBlur?.(e);
endHold();
}}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onPointerCancel={(e) => {
onPointerCancel?.(e);
endHold();
}}
onPointerDown={handlePointerDown}
onPointerLeave={(e) => {
onPointerLeave?.(e);
endHold();
}}
onPointerUp={(e) => {
onPointerUp?.(e);
endHold();
}}
type={type}
>
{/* The fill is the button's ink at low strength, revealed left to
right. It hides once the whole button turns solid. */}
<motion.span
aria-hidden="true"
className="pointer-events-none absolute inset-0 bg-current/15 transition-opacity duration-150 group-data-complete/hold:opacity-0 dark:bg-current/20"
style={{ clipPath }}
/>
<span className="relative flex items-center justify-center overflow-hidden">
<AnimatePresence initial={false} mode="popLayout">
<motion.span
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
className="inline-flex items-center gap-2 whitespace-nowrap"
exit={{ opacity: 0, y: -10, filter: "blur(2px)" }}
initial={{ opacity: 0, y: 10, filter: "blur(2px)" }}
key={labelKey}
transition={LABEL_TRANSITION}
>
{labelKey === "complete" ? (
<DrawnCheck />
) : (
<Icon aria-hidden="true" className="size-4" />
)}
{label}
</motion.span>
</AnimatePresence>
</span>
<span aria-live="polite" className="sr-only">
{phase === "complete"
? (completeLabel ?? defaults.completeLabel)
: ""}
</span>
</button>
</MotionConfig>
);
}
lib/utils.ts
import { type ClassValue, 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/hold-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;
--color-red-50: oklch(97.1% 0.013 17.38);
--color-red-100: oklch(93.6% 0.032 17.717);
--color-red-200: oklch(88.5% 0.062 18.334);
--color-red-300: oklch(80.8% 0.114 19.571);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-red-700: oklch(50.5% 0.213 27.518);
--color-orange-50: oklch(98% 0.016 73.684);
--color-orange-100: oklch(95.4% 0.038 75.164);
--color-orange-200: oklch(90.1% 0.076 70.697);
--color-orange-300: oklch(83.7% 0.128 66.29);
--color-orange-500: oklch(70.5% 0.213 47.604);
--color-orange-600: oklch(64.6% 0.222 41.116);
--color-orange-700: oklch(55.3% 0.195 38.402);
--color-emerald-50: oklch(97.9% 0.021 166.113);
--color-emerald-100: oklch(95% 0.052 163.051);
--color-emerald-200: oklch(90.5% 0.093 164.15);
--color-emerald-300: oklch(84.5% 0.143 164.978);
--color-emerald-500: oklch(69.6% 0.17 162.48);
--color-emerald-600: oklch(59.6% 0.145 163.225);
--color-emerald-700: oklch(50.8% 0.118 165.612);
--color-blue-50: oklch(97% 0.014 254.604);
--color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-200: oklch(88.2% 0.059 254.128);
--color-blue-300: oklch(80.9% 0.105 251.813);
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376);
--color-zinc-50: oklch(98.5% 0 none);
--color-zinc-100: oklch(96.7% 0.001 286.375);
--color-zinc-200: oklch(92% 0.004 286.32);
--color-zinc-300: oklch(87.1% 0.006 286.286);
--color-zinc-700: oklch(37% 0.013 285.805);
--color-zinc-900: oklch(21% 0.006 285.885);
--color-zinc-950: oklch(14.1% 0.005 285.823);
--color-white: #fff;
--spacing: 0.25rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--font-weight-medium: 500;
--radius-lg: var(--radius);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--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;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border-width: 0;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.inset-0 {
inset: 0px;
}
.block {
display: block;
}
.flex {
display: flex;
}
.inline-flex {
display: inline-flex;
}
.size-4 {
width: calc(var(--spacing) * 4);
height: calc(var(--spacing) * 4);
}
.h-10 {
height: calc(var(--spacing) * 10);
}
.min-w-44 {
min-width: calc(var(--spacing) * 44);
}
.touch-none {
touch-action: none;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.gap-3 {
gap: calc(var(--spacing) * 3);
}
.overflow-hidden {
overflow: hidden;
}
.rounded-lg {
border-radius: var(--radius-lg);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-blue-200 {
border-color: var(--color-blue-200);
}
.border-emerald-200 {
border-color: var(--color-emerald-200);
}
.border-orange-200 {
border-color: var(--color-orange-200);
}
.border-red-200 {
border-color: var(--color-red-200);
}
.border-zinc-200 {
border-color: var(--color-zinc-200);
}
.bg-blue-50 {
background-color: var(--color-blue-50);
}
.bg-current\/15 {
background-color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, currentcolor 15%, transparent);
}
}
.bg-emerald-50 {
background-color: var(--color-emerald-50);
}
.bg-orange-50 {
background-color: var(--color-orange-50);
}
.bg-red-50 {
background-color: var(--color-red-50);
}
.bg-zinc-50 {
background-color: var(--color-zinc-50);
}
.px-4 {
padding-inline: calc(var(--spacing) * 4);
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-blue-700 {
color: var(--color-blue-700);
}
.text-emerald-700 {
color: var(--color-emerald-700);
}
.text-orange-700 {
color: var(--color-orange-700);
}
.text-red-700 {
color: var(--color-red-700);
}
.text-zinc-700 {
color: var(--color-zinc-700);
}
.filter {
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.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));
}
.transition-\[background-color\,border-color\,color\] {
transition-property: background-color,border-color,color;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.transition-opacity {
transition-property: opacity;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.duration-150 {
--tw-duration: 150ms;
transition-duration: 150ms;
}
.ease-out {
--tw-ease: var(--ease-out);
transition-timing-function: var(--ease-out);
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
.\[-webkit-touch-callout\:none\] {
-webkit-touch-callout: none;
}
.group-data-complete\/hold\:opacity-0:is(:where(.group\/hold)[data-complete] *) {
opacity: 0%;
}
@media (hover: hover) {
.hover\:bg-blue-100\/70:hover {
background-color: color-mix(in srgb, oklch(93.2% 0.032 255.585) 70%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-blue-100\/70:hover {
background-color: color-mix(in oklab, var(--color-blue-100) 70%, transparent);
}
}
.hover\:bg-emerald-100\/70:hover {
background-color: color-mix(in srgb, oklch(95% 0.052 163.051) 70%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-emerald-100\/70:hover {
background-color: color-mix(in oklab, var(--color-emerald-100) 70%, transparent);
}
}
.hover\:bg-orange-100\/70:hover {
background-color: color-mix(in srgb, oklch(95.4% 0.038 75.164) 70%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-orange-100\/70:hover {
background-color: color-mix(in oklab, var(--color-orange-100) 70%, transparent);
}
}
.hover\:bg-red-100\/70:hover {
background-color: color-mix(in srgb, oklch(93.6% 0.032 17.717) 70%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-red-100\/70:hover {
background-color: color-mix(in oklab, var(--color-red-100) 70%, transparent);
}
}
.hover\:bg-zinc-100\/70:hover {
background-color: color-mix(in srgb, oklch(96.7% 0.001 286.375) 70%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.hover\:bg-zinc-100\/70:hover {
background-color: color-mix(in oklab, var(--color-zinc-100) 70%, transparent);
}
}
}
.focus-visible\:ring-2:focus-visible {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + 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-current\/40:focus-visible {
--tw-ring-color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
--tw-ring-color: color-mix(in oklab, currentcolor 40%, transparent);
}
}
.focus-visible\:ring-offset-2:focus-visible {
--tw-ring-offset-width: 2px;
--tw-ring-offset-shadow: var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
}
.focus-visible\:ring-offset-white:focus-visible {
--tw-ring-offset-color: var(--color-white);
}
.focus-visible\:outline-hidden:focus-visible {
--tw-outline-style: none;
outline-style: none;
@media (forced-colors: active) {
outline: 2px solid transparent;
outline-offset: 2px;
}
}
.disabled\:pointer-events-none:disabled {
pointer-events: none;
}
.disabled\:opacity-50:disabled {
opacity: 50%;
}
.data-complete\:border-blue-600[data-complete] {
border-color: var(--color-blue-600);
}
.data-complete\:border-emerald-600[data-complete] {
border-color: var(--color-emerald-600);
}
.data-complete\:border-orange-600[data-complete] {
border-color: var(--color-orange-600);
}
.data-complete\:border-red-600[data-complete] {
border-color: var(--color-red-600);
}
.data-complete\:border-zinc-900[data-complete] {
border-color: var(--color-zinc-900);
}
.data-complete\:bg-blue-600[data-complete] {
background-color: var(--color-blue-600);
}
.data-complete\:bg-emerald-600[data-complete] {
background-color: var(--color-emerald-600);
}
.data-complete\:bg-orange-600[data-complete] {
background-color: var(--color-orange-600);
}
.data-complete\:bg-red-600[data-complete] {
background-color: var(--color-red-600);
}
.data-complete\:bg-zinc-900[data-complete] {
background-color: var(--color-zinc-900);
}
.data-complete\:text-white[data-complete] {
color: var(--color-white);
}
.dark\:border-blue-500\/25:where(.dark, .dark *) {
border-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 25%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-blue-500) 25%, transparent);
}
}
.dark\:border-emerald-500\/25:where(.dark, .dark *) {
border-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 25%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-emerald-500) 25%, transparent);
}
}
.dark\:border-orange-500\/25:where(.dark, .dark *) {
border-color: color-mix(in srgb, oklch(70.5% 0.213 47.604) 25%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-orange-500) 25%, transparent);
}
}
.dark\:border-red-500\/25:where(.dark, .dark *) {
border-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 25%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-red-500) 25%, transparent);
}
}
.dark\:border-white\/10:where(.dark, .dark *) {
border-color: color-mix(in srgb, #fff 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-white) 10%, transparent);
}
}
.dark\:bg-blue-500\/10:where(.dark, .dark *) {
background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-blue-500) 10%, transparent);
}
}
.dark\:bg-current\/20:where(.dark, .dark *) {
background-color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, currentcolor 20%, transparent);
}
}
.dark\:bg-emerald-500\/10:where(.dark, .dark *) {
background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-emerald-500) 10%, transparent);
}
}
.dark\:bg-orange-500\/10:where(.dark, .dark *) {
background-color: color-mix(in srgb, oklch(70.5% 0.213 47.604) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-orange-500) 10%, transparent);
}
}
.dark\:bg-red-500\/10:where(.dark, .dark *) {
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 10%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-red-500) 10%, transparent);
}
}
.dark\:bg-white\/5:where(.dark, .dark *) {
background-color: color-mix(in srgb, #fff 5%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 5%, transparent);
}
}
.dark\:text-blue-300:where(.dark, .dark *) {
color: var(--color-blue-300);
}
.dark\:text-emerald-300:where(.dark, .dark *) {
color: var(--color-emerald-300);
}
.dark\:text-orange-300:where(.dark, .dark *) {
color: var(--color-orange-300);
}
.dark\:text-red-300:where(.dark, .dark *) {
color: var(--color-red-300);
}
.dark\:text-zinc-300:where(.dark, .dark *) {
color: var(--color-zinc-300);
}
@media (hover: hover) {
.dark\:hover\:bg-blue-500\/15:where(.dark, .dark *):hover {
background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 15%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-blue-500\/15:where(.dark, .dark *):hover {
background-color: color-mix(in oklab, var(--color-blue-500) 15%, transparent);
}
}
.dark\:hover\:bg-emerald-500\/15:where(.dark, .dark *):hover {
background-color: color-mix(in srgb, oklch(69.6% 0.17 162.48) 15%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-emerald-500\/15:where(.dark, .dark *):hover {
background-color: color-mix(in oklab, var(--color-emerald-500) 15%, transparent);
}
}
.dark\:hover\:bg-orange-500\/15:where(.dark, .dark *):hover {
background-color: color-mix(in srgb, oklch(70.5% 0.213 47.604) 15%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-orange-500\/15:where(.dark, .dark *):hover {
background-color: color-mix(in oklab, var(--color-orange-500) 15%, transparent);
}
}
.dark\:hover\:bg-red-500\/15:where(.dark, .dark *):hover {
background-color: color-mix(in srgb, oklch(63.7% 0.237 25.331) 15%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-red-500\/15:where(.dark, .dark *):hover {
background-color: color-mix(in oklab, var(--color-red-500) 15%, transparent);
}
}
.dark\:hover\:bg-white\/8:where(.dark, .dark *):hover {
background-color: color-mix(in srgb, #fff 8%, transparent);
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:hover\:bg-white\/8:where(.dark, .dark *):hover {
background-color: color-mix(in oklab, var(--color-white) 8%, transparent);
}
}
}
.dark\:focus-visible\:ring-offset-zinc-950:where(.dark, .dark *):focus-visible {
--tw-ring-offset-color: var(--color-zinc-950);
}
.dark\:data-complete\:border-blue-500:where(.dark, .dark *)[data-complete] {
border-color: var(--color-blue-500);
}
.dark\:data-complete\:border-emerald-500:where(.dark, .dark *)[data-complete] {
border-color: var(--color-emerald-500);
}
.dark\:data-complete\:border-orange-500:where(.dark, .dark *)[data-complete] {
border-color: var(--color-orange-500);
}
.dark\:data-complete\:border-red-500:where(.dark, .dark *)[data-complete] {
border-color: var(--color-red-500);
}
.dark\:data-complete\:border-zinc-100:where(.dark, .dark *)[data-complete] {
border-color: var(--color-zinc-100);
}
.dark\:data-complete\:bg-blue-500:where(.dark, .dark *)[data-complete] {
background-color: var(--color-blue-500);
}
.dark\:data-complete\:bg-emerald-500:where(.dark, .dark *)[data-complete] {
background-color: var(--color-emerald-500);
}
.dark\:data-complete\:bg-orange-500:where(.dark, .dark *)[data-complete] {
background-color: var(--color-orange-500);
}
.dark\:data-complete\:bg-red-500:where(.dark, .dark *)[data-complete] {
background-color: var(--color-red-500);
}
.dark\:data-complete\:bg-zinc-100:where(.dark, .dark *)[data-complete] {
background-color: var(--color-zinc-100);
}
.dark\:data-complete\:text-white:where(.dark, .dark *)[data-complete] {
color: var(--color-white);
}
.dark\:data-complete\:text-zinc-900:where(.dark, .dark *)[data-complete] {
color: var(--color-zinc-900);
}
}
: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;
--color-fd-background: #000;
--color-fd-card: #0a0a0a;
--color-fd-popover: #0a0a0a;
--color-fd-border: rgb(255 255 255 / 0.14);
--color-fd-accent: rgb(255 255 255 / 0.07);
}
:root {
--sidebar: hsl(0 0% 98%);
--sidebar-foreground: hsl(240 5.3% 26.1%);
--sidebar-primary: hsl(240 5.9% 10%);
--sidebar-primary-foreground: hsl(0 0% 98%);
--sidebar-accent: hsl(240 4.8% 95.9%);
--sidebar-accent-foreground: hsl(240 5.9% 10%);
--sidebar-border: hsl(220 13% 91%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
}
.dark {
--sidebar: hsl(240 5.9% 10%);
--sidebar-foreground: hsl(240 4.8% 95.9%);
--sidebar-primary: hsl(224.3 76.3% 48%);
--sidebar-primary-foreground: hsl(0 0% 100%);
--sidebar-accent: hsl(240 3.7% 15.9%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
--sidebar-border: hsl(240 3.7% 15.9%);
--sidebar-ring: hsl(217.2 91.2% 59.8%);
}
:root {
--leaf: #1fa834;
}
.dark {
--leaf: #3bb44b;
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
}
@property --tw-blur {
syntax: "*";
inherits: false;
}
@property --tw-brightness {
syntax: "*";
inherits: false;
}
@property --tw-contrast {
syntax: "*";
inherits: false;
}
@property --tw-grayscale {
syntax: "*";
inherits: false;
}
@property --tw-hue-rotate {
syntax: "*";
inherits: false;
}
@property --tw-invert {
syntax: "*";
inherits: false;
}
@property --tw-opacity {
syntax: "*";
inherits: false;
}
@property --tw-saturate {
syntax: "*";
inherits: false;
}
@property --tw-sepia {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-drop-shadow-size {
syntax: "*";
inherits: false;
}
@property --tw-duration {
syntax: "*";
inherits: false;
}
@property --tw-ease {
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;
}
@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-font-weight: initial;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
--tw-grayscale: initial;
--tw-hue-rotate: initial;
--tw-invert: initial;
--tw-opacity: initial;
--tw-saturate: initial;
--tw-sepia: initial;
--tw-drop-shadow: initial;
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
--tw-duration: initial;
--tw-ease: 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;
}
}
}