diff --git a/packages/components/spinner/src/use-spinner.ts b/packages/components/spinner/src/use-spinner.ts
index 6283e95d7a..4eed495876 100644
--- a/packages/components/spinner/src/use-spinner.ts
+++ b/packages/components/spinner/src/use-spinner.ts
@@ -5,6 +5,7 @@ import {mapPropsVariants} from "@heroui/system-rsc";
import {spinner} from "@heroui/theme";
import {clsx, objectToDeps} from "@heroui/shared-utils";
import {useMemo, useCallback, Ref} from "react";
+import {useProviderContext} from "@heroui/system";
interface Props extends HTMLHeroUIProps<"div"> {
/**
@@ -38,6 +39,9 @@ export type UseSpinnerProps = Props & SpinnerVariantProps;
export function useSpinner(originalProps: UseSpinnerProps) {
const [props, variantProps] = mapPropsVariants(originalProps, spinner.variantKeys);
+ const globalContext = useProviderContext();
+ const variant = originalProps?.variant ?? globalContext?.spinnerVariant ?? "default";
+
const {children, className, classNames, label: labelProp, ...otherProps} = props;
const slots = useMemo(() => spinner({...variantProps}), [objectToDeps(variantProps)]);
@@ -65,7 +69,7 @@ export function useSpinner(originalProps: UseSpinnerProps) {
[ariaLabel, slots, baseStyles, otherProps],
);
- return {label, slots, classNames, getSpinnerProps};
+ return {label, slots, classNames, variant, getSpinnerProps};
}
export type UseSpinnerReturn = ReturnType
;
diff --git a/packages/components/spinner/stories/spinner.stories.tsx b/packages/components/spinner/stories/spinner.stories.tsx
index 323698f250..de9c704632 100644
--- a/packages/components/spinner/stories/spinner.stories.tsx
+++ b/packages/components/spinner/stories/spinner.stories.tsx
@@ -2,7 +2,7 @@ import React from "react";
import {Meta} from "@storybook/react";
import {spinner} from "@heroui/theme";
-import {Spinner} from "../src";
+import {Spinner, SpinnerProps} from "../src";
export default {
title: "Components/Spinner",
@@ -26,6 +26,12 @@ export default {
},
options: ["sm", "md", "lg"],
},
+ variant: {
+ control: {
+ type: "select",
+ },
+ options: ["default", "gradient", "spinner", "wave", "dots"],
+ },
},
decorators: [
(Story) => (
@@ -40,6 +46,18 @@ const defaultProps = {
...spinner.defaultVariants,
};
+const VariantsTemplate = (args: SpinnerProps) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
export const Default = {
args: {
...defaultProps,
@@ -52,3 +70,14 @@ export const WithLabel = {
label: "Loading...",
},
};
+
+export const Variants = {
+ args: {
+ ...defaultProps,
+ classNames: {
+ label: "text-primary-400 mt-4",
+ },
+ },
+
+ render: VariantsTemplate,
+};
diff --git a/packages/core/system/src/provider-context.ts b/packages/core/system/src/provider-context.ts
index 45e8e2e1fd..bffdbd3ec5 100644
--- a/packages/core/system/src/provider-context.ts
+++ b/packages/core/system/src/provider-context.ts
@@ -1,4 +1,4 @@
-import type {SupportedCalendars} from "./types";
+import type {SpinnerVariants, SupportedCalendars} from "./types";
import type {Calendar} from "@internationalized/date";
import type {DateValue} from "@react-types/datepicker";
@@ -87,6 +87,11 @@ export type ProviderContextProps = {
* @default all calendars
*/
createCalendar?: (calendar: SupportedCalendars) => Calendar | null;
+ /**
+ * The default variant of the spinner.
+ * @default default
+ */
+ spinnerVariant?: SpinnerVariants;
};
export const [ProviderContext, useProviderContext] = createContext({
diff --git a/packages/core/system/src/provider.tsx b/packages/core/system/src/provider.tsx
index ac70161702..84726cddff 100644
--- a/packages/core/system/src/provider.tsx
+++ b/packages/core/system/src/provider.tsx
@@ -63,6 +63,7 @@ export const HeroUIProvider: React.FC = ({
// then they will be set in `use-date-input.ts` or `use-calendar-base.ts`
defaultDates,
createCalendar,
+ spinnerVariant,
...otherProps
}) => {
let contents = children;
@@ -87,6 +88,7 @@ export const HeroUIProvider: React.FC = ({
disableRipple,
validationBehavior,
labelPlacement,
+ spinnerVariant,
};
}, [
createCalendar,
@@ -96,6 +98,7 @@ export const HeroUIProvider: React.FC = ({
disableRipple,
validationBehavior,
labelPlacement,
+ spinnerVariant,
]);
return (
diff --git a/packages/core/system/src/types.ts b/packages/core/system/src/types.ts
index a8bdae8bd1..66fcfa5ee1 100644
--- a/packages/core/system/src/types.ts
+++ b/packages/core/system/src/types.ts
@@ -15,3 +15,8 @@ export type SupportedCalendars =
| "persian"
| "roc"
| "gregory";
+
+/**
+ * Spinner Variants
+ */
+export type SpinnerVariants = "default" | "gradient" | "wave" | "dots" | "spinner";
diff --git a/packages/core/theme/src/animations/index.ts b/packages/core/theme/src/animations/index.ts
index 9b80f09f01..a47304d4b5 100644
--- a/packages/core/theme/src/animations/index.ts
+++ b/packages/core/theme/src/animations/index.ts
@@ -3,6 +3,9 @@ export const animations = {
"drip-expand": "drip-expand 420ms linear",
"spinner-ease-spin": "spinner-spin 0.8s ease infinite",
"spinner-linear-spin": "spinner-spin 0.8s linear infinite",
+ sway: "sway 750ms ease infinite",
+ blink: "blink 1.4s infinite both",
+ "fade-out": "fade-out 1.2s linear 0s infinite normal none running",
"appearance-in": "appearance-in 250ms ease-out normal both",
"appearance-out": "appearance-out 60ms ease-in normal both",
"indeterminate-bar":
@@ -67,5 +70,35 @@ export const animations = {
transform: "translateX(100%) scaleX(1)",
},
},
+ sway: {
+ "0%": {
+ transform: "translate(0px, 0px)",
+ },
+ "50%": {
+ transform: "translate(0px, -150%)",
+ },
+ "100%": {
+ transform: "translate(0px, 0px)",
+ },
+ },
+ blink: {
+ "0%": {
+ opacity: "0.2",
+ },
+ "20%": {
+ opacity: "1",
+ },
+ "100%": {
+ opacity: "0.2",
+ },
+ },
+ "fade-out": {
+ "0%": {
+ opacity: "1",
+ },
+ "100%": {
+ opacity: "0.15",
+ },
+ },
},
};
diff --git a/packages/core/theme/src/components/spinner.ts b/packages/core/theme/src/components/spinner.ts
index 8173804b29..83a1a4f7ec 100644
--- a/packages/core/theme/src/components/spinner.ts
+++ b/packages/core/theme/src/components/spinner.ts
@@ -18,32 +18,20 @@ const spinner = tv({
slots: {
base: "relative inline-flex flex-col gap-2 items-center justify-center",
wrapper: "relative flex",
- circle1: [
- "absolute",
- "w-full",
- "h-full",
- "rounded-full",
- "animate-spinner-ease-spin",
- "border-2",
- "border-solid",
- "border-t-transparent",
- "border-l-transparent",
- "border-r-transparent",
- ],
- circle2: [
+ label: "text-foreground dark:text-foreground-dark font-regular",
+ circle1: "absolute w-full h-full rounded-full",
+ circle2: "absolute w-full h-full rounded-full",
+ dots: "relative rounded-full mx-auto",
+ spinnerBars: [
"absolute",
- "w-full",
- "h-full",
+ "animate-fade-out",
"rounded-full",
- "opacity-75",
- "animate-spinner-linear-spin",
- "border-2",
- "border-dotted",
- "border-t-transparent",
- "border-l-transparent",
- "border-r-transparent",
+ "w-[25%]",
+ "h-[8%]",
+ "left-[calc(37.5%)]",
+ "top-[calc(46%)]",
+ "spinner-bar-animation",
],
- label: "text-foreground dark:text-foreground-dark font-regular",
},
variants: {
size: {
@@ -51,18 +39,21 @@ const spinner = tv({
wrapper: "w-5 h-5",
circle1: "border-2",
circle2: "border-2",
+ dots: "size-1",
label: "text-small",
},
md: {
wrapper: "w-8 h-8",
circle1: "border-3",
circle2: "border-3",
+ dots: "size-1.5",
label: "text-medium",
},
lg: {
wrapper: "w-10 h-10",
circle1: "border-3",
circle2: "border-3",
+ dots: "size-2",
label: "text-large",
},
},
@@ -70,34 +61,50 @@ const spinner = tv({
current: {
circle1: "border-b-current",
circle2: "border-b-current",
+ dots: "bg-current",
+ spinnerBars: "bg-current",
},
white: {
circle1: "border-b-white",
circle2: "border-b-white",
+ dots: "bg-white",
+ spinnerBars: "bg-white",
},
default: {
circle1: "border-b-default",
circle2: "border-b-default",
+ dots: "bg-default",
+ spinnerBars: "bg-default",
},
primary: {
circle1: "border-b-primary",
circle2: "border-b-primary",
+ dots: "bg-primary",
+ spinnerBars: "bg-primary",
},
secondary: {
circle1: "border-b-secondary",
circle2: "border-b-secondary",
+ dots: "bg-secondary",
+ spinnerBars: "bg-secondary",
},
success: {
circle1: "border-b-success",
circle2: "border-b-success",
+ dots: "bg-success",
+ spinnerBars: "bg-success",
},
warning: {
circle1: "border-b-warning",
circle2: "border-b-warning",
+ dots: "bg-warning",
+ spinnerBars: "bg-warning",
},
danger: {
circle1: "border-b-danger",
circle2: "border-b-danger",
+ dots: "bg-danger",
+ spinnerBars: "bg-danger",
},
},
labelColor: {
@@ -120,12 +127,106 @@ const spinner = tv({
label: "text-danger",
},
},
+ variant: {
+ default: {
+ circle1: [
+ "animate-spinner-ease-spin",
+ "border-solid",
+ "border-t-transparent",
+ "border-l-transparent",
+ "border-r-transparent",
+ ],
+ circle2: [
+ "opacity-75",
+ "animate-spinner-linear-spin",
+ "border-dotted",
+ "border-t-transparent",
+ "border-l-transparent",
+ "border-r-transparent",
+ ],
+ },
+ gradient: {
+ circle1: [
+ "border-0",
+ "bg-gradient-to-b",
+ "from-transparent",
+ "via-transparent",
+ "to-primary",
+ "animate-spinner-linear-spin",
+ "[animation-duration:1s]",
+ "[-webkit-mask:radial-gradient(closest-side,rgba(0,0,0,0.0)calc(100%-3px),rgba(0,0,0,1)calc(100%-3px))]",
+ ],
+ circle2: ["hidden"],
+ },
+ wave: {
+ wrapper: "translate-y-3/4",
+ dots: ["animate-sway", "spinner-dot-animation"],
+ },
+ dots: {
+ wrapper: "translate-y-2/4",
+ dots: ["animate-blink", "spinner-dot-blink-animation"],
+ },
+ spinner: {},
+ },
},
defaultVariants: {
size: "md",
color: "primary",
labelColor: "foreground",
+ variant: "default",
},
+ compoundVariants: [
+ {variant: "gradient", color: "current", class: {circle1: "to-current"}},
+ {variant: "gradient", color: "white", class: {circle1: "to-white"}},
+ {variant: "gradient", color: "default", class: {circle1: "to-default"}},
+ {variant: "gradient", color: "primary", class: {circle1: "to-primary"}},
+ {variant: "gradient", color: "secondary", class: {circle1: "to-secondary"}},
+ {variant: "gradient", color: "success", class: {circle1: "to-success"}},
+ {variant: "gradient", color: "warning", class: {circle1: "to-warning"}},
+ {variant: "gradient", color: "danger", class: {circle1: "to-danger"}},
+ {
+ variant: "wave",
+ size: "sm",
+ class: {
+ wrapper: "w-5 h-5",
+ },
+ },
+ {
+ variant: "wave",
+ size: "md",
+ class: {
+ wrapper: "w-8 h-8",
+ },
+ },
+ {
+ variant: "wave",
+ size: "lg",
+ class: {
+ wrapper: "w-12 h-12",
+ },
+ },
+ {
+ variant: "dots",
+ size: "sm",
+ class: {
+ wrapper: "w-5 h-5",
+ },
+ },
+ {
+ variant: "dots",
+ size: "md",
+ class: {
+ wrapper: "w-8 h-8",
+ },
+ },
+ {
+ variant: "dots",
+ size: "lg",
+ class: {
+ wrapper: "w-12 h-12",
+ },
+ },
+ ],
});
export type SpinnerVariantProps = VariantProps;
diff --git a/packages/core/theme/src/utilities/animation.ts b/packages/core/theme/src/utilities/animation.ts
new file mode 100644
index 0000000000..2437f43290
--- /dev/null
+++ b/packages/core/theme/src/utilities/animation.ts
@@ -0,0 +1,13 @@
+export default {
+ /** Animation Utilities */
+ ".spinner-bar-animation": {
+ "animation-delay": "calc(-1.2s + (0.1s * var(--bar-index)))",
+ transform: "rotate(calc(30deg * var(--bar-index)))translate(140%)",
+ },
+ ".spinner-dot-animation": {
+ "animation-delay": "calc(250ms * var(--dot-index))",
+ },
+ ".spinner-dot-blink-animation": {
+ "animation-delay": "calc(200ms * var(--dot-index))",
+ },
+};
diff --git a/packages/core/theme/src/utilities/index.ts b/packages/core/theme/src/utilities/index.ts
index 0c0c2fd28c..7248844bc2 100644
--- a/packages/core/theme/src/utilities/index.ts
+++ b/packages/core/theme/src/utilities/index.ts
@@ -1,9 +1,11 @@
import transition from "./transition";
import custom from "./custom";
import scrollbarHide from "./scrollbar-hide";
+import animation from "./animation";
export const utilities = {
...custom,
...transition,
...scrollbarHide,
+ ...animation,
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2a59bc0532..a184cf9fc5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2785,6 +2785,9 @@ importers:
'@heroui/shared-utils':
specifier: workspace:*
version: link:../../utilities/shared-utils
+ '@heroui/system':
+ specifier: workspace:*
+ version: link:../../core/system
'@heroui/system-rsc':
specifier: workspace:*
version: link:../../core/system-rsc