((props, ref) => {
+ const {domRef, slots, styles, label, getSpinnerProps} = useSpinner({ref, ...props});
+
+ return (
+
+
+
+ {label && {label}}
+
+ );
+});
+
+if (__DEV__) {
+ Spinner.displayName = "NextUI.Loading";
+}
+
+export default Spinner;
diff --git a/packages/components/spinner/src/use-spinner.ts b/packages/components/spinner/src/use-spinner.ts
new file mode 100644
index 000000000..d7f9f0a24
--- /dev/null
+++ b/packages/components/spinner/src/use-spinner.ts
@@ -0,0 +1,70 @@
+import type {SpinnerVariantProps, SpinnerSlots, SlotsToClasses} from "@nextui-org/theme";
+
+import {HTMLNextUIProps, mapPropsVariants} from "@nextui-org/system";
+import {spinner} from "@nextui-org/theme";
+import {clsx, ReactRef} from "@nextui-org/shared-utils";
+import {useDOMRef} from "@nextui-org/dom-utils";
+import {useMemo, useCallback} from "react";
+
+export interface UseSpinnerProps extends HTMLNextUIProps<"div", SpinnerVariantProps> {
+ /**
+ * Ref to the DOM node.
+ */
+ ref?: ReactRef;
+ /**
+ * Spinner label, in case you passed it will be used as `aria-label`.
+ */
+ label?: string;
+ /**
+ * Classname or List of classes to change the styles of the avatar.
+ * if `className` is passed, it will be added to the base slot.
+ *
+ * @example
+ * ```ts
+ *
+ * ```
+ */
+ styles?: SlotsToClasses;
+}
+
+export function useSpinner(originalProps: UseSpinnerProps) {
+ const [props, variantProps] = mapPropsVariants(originalProps, spinner.variantKeys);
+
+ const {ref, children, className, styles, label: labelProp, ...otherProps} = props;
+
+ const domRef = useDOMRef(ref);
+
+ const slots = useMemo(() => spinner({...variantProps}), [variantProps]);
+
+ const baseStyles = clsx(styles?.base, className);
+
+ const label = labelProp || children;
+
+ const ariaLabel = useMemo(() => {
+ if (label && typeof label === "string") {
+ return label;
+ }
+
+ return !otherProps["aria-label"] ? "Loading" : "";
+ }, [children, label, otherProps["aria-label"]]);
+
+ const getSpinnerProps = useCallback(
+ () => ({
+ "aria-label": ariaLabel,
+ className: slots.base({
+ class: baseStyles,
+ }),
+ ...otherProps,
+ }),
+ [ariaLabel, slots, baseStyles, otherProps],
+ );
+
+ return {domRef, label, slots, styles, getSpinnerProps};
+}
+
+export type UseSpinnerReturn = ReturnType;
diff --git a/packages/components/spinner/stories/spinner.stories.tsx b/packages/components/spinner/stories/spinner.stories.tsx
new file mode 100644
index 000000000..297d5a439
--- /dev/null
+++ b/packages/components/spinner/stories/spinner.stories.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+import {ComponentStory, ComponentMeta} from "@storybook/react";
+import {spinner} from "@nextui-org/theme";
+
+import {Spinner, SpinnerProps} from "../src";
+
+export default {
+ title: "Feedback/Spinner",
+ component: Spinner,
+ argTypes: {
+ color: {
+ control: {
+ type: "select",
+ options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
+ },
+ },
+ labelColor: {
+ control: {
+ type: "select",
+ options: ["neutral", "primary", "secondary", "success", "warning", "danger"],
+ },
+ },
+ size: {
+ control: {
+ type: "select",
+ options: ["xs", "sm", "md", "lg", "xl"],
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+} as ComponentMeta;
+
+const defaultProps = {
+ ...spinner.defaultVariants,
+};
+
+const Template: ComponentStory = (args: SpinnerProps) => ;
+
+export const Default = Template.bind({});
+Default.args = {
+ ...defaultProps,
+};
+
+export const WithLabel = Template.bind({});
+WithLabel.args = {
+ ...defaultProps,
+ label: "Loading...",
+};
diff --git a/packages/components/loading/tsconfig.json b/packages/components/spinner/tsconfig.json
similarity index 100%
rename from packages/components/loading/tsconfig.json
rename to packages/components/spinner/tsconfig.json
diff --git a/packages/components/storybook/.storybook/main.js b/packages/components/storybook/.storybook/main.js
index 30d604f0f..c92bf9a6b 100644
--- a/packages/components/storybook/.storybook/main.js
+++ b/packages/components/storybook/.storybook/main.js
@@ -4,6 +4,7 @@ module.exports = {
"../../avatar/stories/*.stories.@(js|jsx|ts|tsx)",
"../../user/stories/*.stories.@(js|jsx|ts|tsx)",
"../../button/stories/*.stories.@(js|jsx|ts|tsx)",
+ "../../spinner/stories/*.stories.@(js|jsx|ts|tsx)",
],
staticDirs: ["../public"],
addons: [
diff --git a/packages/components/user/src/use-user.ts b/packages/components/user/src/use-user.ts
index 16e43fd7c..7e3d89030 100644
--- a/packages/components/user/src/use-user.ts
+++ b/packages/components/user/src/use-user.ts
@@ -76,7 +76,7 @@ export function useUser(props: UseUserProps) {
return isFocusable || as === "button";
}, [isFocusable, as]);
- const slots = user({isFocusVisible});
+ const slots = useMemo(() => user({isFocusVisible}), [isFocusVisible]);
const baseStyles = clsx(styles?.base, className);
diff --git a/packages/core/react/package.json b/packages/core/react/package.json
index 51d392b0b..4342f3601 100644
--- a/packages/core/react/package.json
+++ b/packages/core/react/package.json
@@ -50,7 +50,7 @@
"@nextui-org/drip": "workspace:*",
"@nextui-org/image": "workspace:*",
"@nextui-org/link": "workspace:*",
- "@nextui-org/loading": "workspace:*",
+ "@nextui-org/spinner": "workspace:*",
"@nextui-org/pagination": "workspace:*",
"@nextui-org/radio": "workspace:*",
"@nextui-org/snippet": "workspace:*",
diff --git a/packages/core/theme/src/components/button/index.ts b/packages/core/theme/src/components/button/index.ts
index 3178a6d0f..06fb34e50 100644
--- a/packages/core/theme/src/components/button/index.ts
+++ b/packages/core/theme/src/components/button/index.ts
@@ -26,7 +26,7 @@ const button = tv({
"antialiased",
"active:scale-95",
"overflow-hidden",
- "gap-2",
+ "gap-3",
],
variants: {
variant: {
diff --git a/packages/core/theme/src/components/index.ts b/packages/core/theme/src/components/index.ts
index d914b26f6..15b1c8fc6 100644
--- a/packages/core/theme/src/components/index.ts
+++ b/packages/core/theme/src/components/index.ts
@@ -4,3 +4,4 @@ export * from "./avatar-group";
export * from "./user";
export * from "./button";
export * from "./drip";
+export * from "./spinner";
diff --git a/packages/core/theme/src/components/spinner/index.ts b/packages/core/theme/src/components/spinner/index.ts
new file mode 100644
index 000000000..01667a5bb
--- /dev/null
+++ b/packages/core/theme/src/components/spinner/index.ts
@@ -0,0 +1,138 @@
+import {tv, type VariantProps} from "tailwind-variants";
+
+/**
+ * Spinner wrapper **Tailwind Variants** component
+ *
+ * const {base, line1, line2, label } = spinner({...})
+ *
+ * @example
+ *
+ *
+ *
+ *
+ *
+ */
+const spinner = tv({
+ slots: {
+ base: "flex flex-col items-center justify-center relative",
+ 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: [
+ "absolute",
+ "w-full",
+ "h-full",
+ "rounded-full",
+ "opacity-75",
+ "animate-spinner-linear-spin",
+ "border-2",
+ "border-dotted",
+ "border-t-transparent",
+ "border-l-transparent",
+ "border-r-transparent",
+ ],
+ label: "text-foreground dark:text-foreground-dark font-regular",
+ },
+ variants: {
+ size: {
+ xs: {
+ base: "w-4 h-4",
+ circle1: "border-2",
+ circle2: "border-2",
+ label: "translate-y-6 text-xs",
+ },
+ sm: {
+ base: "w-5 h-5",
+ circle1: "border-2",
+ circle2: "border-2",
+ label: "translate-y-6 text-xs",
+ },
+ md: {
+ base: "w-8 h-8",
+ circle1: "border-[3px]",
+ circle2: "border-[3px]",
+ label: "translate-y-8 text-sm",
+ },
+ lg: {
+ base: "w-10 h-10",
+ circle1: "border-[3px]",
+ circle2: "border-[3px]",
+ label: "translate-y-10 text-base",
+ },
+ xl: {
+ base: "w-12 h-12",
+ circle1: "border-4",
+ circle2: "border-4",
+ label: "translate-y-12 text-lg",
+ },
+ },
+ color: {
+ white: {
+ circle1: "border-b-white",
+ circle2: "border-b-white",
+ },
+ neutral: {
+ circle1: "border-b-neutral",
+ circle2: "border-b-neutral",
+ },
+ primary: {
+ circle1: "border-b-primary",
+ circle2: "border-b-primary",
+ },
+ secondary: {
+ circle1: "border-b-secondary",
+ circle2: "border-b-secondary",
+ },
+ success: {
+ circle1: "border-b-success",
+ circle2: "border-b-success",
+ },
+ warning: {
+ circle1: "border-b-warning",
+ circle2: "border-b-warning",
+ },
+ danger: {
+ circle1: "border-b-danger",
+ circle2: "border-b-danger",
+ },
+ },
+ labelColor: {
+ neutral: {
+ label: "text-neutral",
+ },
+ primary: {
+ label: "text-primary",
+ },
+ secondary: {
+ label: "text-secondary",
+ },
+ success: {
+ label: "text-success",
+ },
+ warning: {
+ label: "text-warning",
+ },
+ danger: {
+ label: "text-danger",
+ },
+ },
+ },
+ defaultVariants: {
+ size: "md",
+ color: "primary",
+ },
+});
+
+export type SpinnerVariantProps = VariantProps;
+export type SpinnerSlots = keyof ReturnType;
+
+export {spinner};
diff --git a/packages/core/theme/src/plugin.js b/packages/core/theme/src/plugin.js
index 430f55c4c..cccb548ca 100644
--- a/packages/core/theme/src/plugin.js
+++ b/packages/core/theme/src/plugin.js
@@ -153,8 +153,18 @@ module.exports = plugin(
},
animation: {
"drip-expand": "drip-expand 350ms linear",
+ "spinner-ease-spin": "spinner-spin 0.8s ease infinite",
+ "spinner-linear-spin": "spinner-spin 0.8s linear infinite",
},
keyframes: {
+ "spinner-spin": {
+ "0%": {
+ transform: "rotate(0deg)",
+ },
+ "100%": {
+ transform: "rotate(360deg)",
+ },
+ },
"drip-expand": {
"0%": {
opacity: 0,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3dfa09850..985b65535 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -383,9 +383,9 @@ importers:
specifiers:
'@nextui-org/dom-utils': workspace:*
'@nextui-org/drip': workspace:*
- '@nextui-org/loading': workspace:*
'@nextui-org/shared-icons': workspace:*
'@nextui-org/shared-utils': workspace:*
+ '@nextui-org/spinner': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
'@react-aria/button': ^3.6.2
@@ -400,6 +400,7 @@ importers:
'@nextui-org/dom-utils': link:../../utilities/dom-utils
'@nextui-org/drip': link:../drip
'@nextui-org/shared-utils': link:../../utilities/shared-utils
+ '@nextui-org/spinner': link:../spinner
'@nextui-org/system': link:../../core/system
'@nextui-org/theme': link:../../core/theme
'@react-aria/button': 3.6.4_react@17.0.2
@@ -407,7 +408,6 @@ importers:
'@react-aria/interactions': 3.13.1_react@17.0.2
'@react-aria/utils': 3.14.2_react@17.0.2
devDependencies:
- '@nextui-org/loading': link:../loading
'@nextui-org/shared-icons': link:../../utilities/shared-icons
'@react-types/button': 3.7.0_react@17.0.2
'@react-types/shared': 3.16.0_react@17.0.2
@@ -593,21 +593,6 @@ importers:
clean-package: 2.1.1
react: 17.0.2
- packages/components/loading:
- specifiers:
- '@nextui-org/dom-utils': workspace:*
- '@nextui-org/shared-utils': workspace:*
- '@nextui-org/system': workspace:*
- clean-package: 2.1.1
- react: ^17.0.2
- dependencies:
- '@nextui-org/dom-utils': link:../../utilities/dom-utils
- '@nextui-org/shared-utils': link:../../utilities/shared-utils
- '@nextui-org/system': link:../../core/system
- devDependencies:
- clean-package: 2.1.1
- react: 17.0.2
-
packages/components/pagination:
specifiers:
'@nextui-org/dom-utils': workspace:*
@@ -681,6 +666,23 @@ importers:
clean-package: 2.1.1
react: 17.0.2
+ packages/components/spinner:
+ specifiers:
+ '@nextui-org/dom-utils': workspace:*
+ '@nextui-org/shared-utils': workspace:*
+ '@nextui-org/system': workspace:*
+ '@nextui-org/theme': workspace:*
+ clean-package: 2.1.1
+ react: ^17.0.2
+ dependencies:
+ '@nextui-org/dom-utils': link:../../utilities/dom-utils
+ '@nextui-org/shared-utils': link:../../utilities/shared-utils
+ '@nextui-org/system': link:../../core/system
+ '@nextui-org/theme': link:../../core/theme
+ devDependencies:
+ clean-package: 2.1.1
+ react: 17.0.2
+
packages/components/storybook:
specifiers:
'@babel/core': ^7.16.7
@@ -770,10 +772,10 @@ importers:
'@nextui-org/drip': workspace:*
'@nextui-org/image': workspace:*
'@nextui-org/link': workspace:*
- '@nextui-org/loading': workspace:*
'@nextui-org/pagination': workspace:*
'@nextui-org/radio': workspace:*
'@nextui-org/snippet': workspace:*
+ '@nextui-org/spinner': workspace:*
'@nextui-org/system': workspace:*
'@nextui-org/theme': workspace:*
'@nextui-org/user': workspace:*
@@ -790,10 +792,10 @@ importers:
'@nextui-org/drip': link:../../components/drip
'@nextui-org/image': link:../../components/image
'@nextui-org/link': link:../../components/link
- '@nextui-org/loading': link:../../components/loading
'@nextui-org/pagination': link:../../components/pagination
'@nextui-org/radio': link:../../components/radio
'@nextui-org/snippet': link:../../components/snippet
+ '@nextui-org/spinner': link:../../components/spinner
'@nextui-org/system': link:../system
'@nextui-org/theme': link:../theme
'@nextui-org/user': link:../../components/user
@@ -5340,7 +5342,7 @@ packages:
'@babel/runtime': 7.20.13
'@react-aria/focus': 3.10.1_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/toggle': 3.4.2_react@17.0.2
'@react-types/button': 3.7.0_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
@@ -5370,7 +5372,7 @@ packages:
'@babel/runtime': 7.20.13
'@react-aria/label': 3.4.2_react@17.0.2
'@react-aria/toggle': 3.4.2_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/checkbox': 3.3.0_react@17.0.2
'@react-stately/toggle': 3.4.2_react@17.0.2
'@react-types/checkbox': 3.4.0_react@17.0.2
@@ -5402,7 +5404,7 @@ packages:
'@babel/runtime': 7.20.13
'@react-aria/focus': 3.10.1_react@17.0.2
'@react-aria/overlays': 3.11.0_sfoxds7t5ydpegc3knd667wn6m
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/overlays': 3.4.2_react@17.0.2
'@react-types/dialog': 3.4.5_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
@@ -5431,7 +5433,7 @@ packages:
dependencies:
'@babel/runtime': 7.20.13
'@react-aria/interactions': 3.12.0_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
clsx: 1.2.1
react: 17.0.2
@@ -5471,7 +5473,7 @@ packages:
'@internationalized/number': 3.1.2
'@internationalized/string': 3.0.1
'@react-aria/ssr': 3.3.0_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
dev: false
@@ -5498,7 +5500,7 @@ packages:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@babel/runtime': 7.20.13
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
dev: false
@@ -5519,7 +5521,7 @@ packages:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@babel/runtime': 7.20.13
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/label': 3.7.1_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
@@ -5545,7 +5547,7 @@ packages:
'@babel/runtime': 7.20.13
'@react-aria/focus': 3.10.1_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/link': 3.3.6_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
react: 17.0.2
@@ -5582,7 +5584,7 @@ packages:
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/overlays': 3.11.0_sfoxds7t5ydpegc3knd667wn6m
'@react-aria/selection': 3.12.1_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/collections': 3.4.4_react@17.0.2
'@react-stately/menu': 3.4.2_react@17.0.2
'@react-stately/tree': 3.3.4_react@17.0.2
@@ -5604,7 +5606,7 @@ packages:
'@react-aria/i18n': 3.6.1_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/ssr': 3.3.0_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-aria/visually-hidden': 3.5.0_react@17.0.2
'@react-stately/overlays': 3.4.2_react@17.0.2
'@react-types/button': 3.7.0_react@17.0.2
@@ -5644,7 +5646,7 @@ packages:
'@react-aria/i18n': 3.6.1_react@17.0.2
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/label': 3.4.2_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/radio': 3.6.0_react@17.0.2
'@react-types/radio': 3.3.1_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
@@ -5714,7 +5716,7 @@ packages:
'@react-aria/interactions': 3.12.0_react@17.0.2
'@react-aria/live-announcer': 3.1.2
'@react-aria/selection': 3.12.1_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-stately/table': 3.5.0_react@17.0.2
'@react-stately/virtualizer': 3.4.1_react@17.0.2
'@react-types/checkbox': 3.4.0_react@17.0.2
@@ -5773,7 +5775,7 @@ packages:
dependencies:
'@babel/runtime': 7.20.13
'@react-aria/interactions': 3.12.0_react@17.0.2
- '@react-aria/utils': 3.14.0_react@17.0.2
+ '@react-aria/utils': 3.14.2_react@17.0.2
'@react-types/shared': 3.15.0_react@17.0.2
clsx: 1.2.1
react: 17.0.2