ui
ui copied to clipboard
Adding a 'loading' prop for buttons
Feature description
I thought it would be really convinient if there was natively a 'loading' prop for buttons. I altered my button component to have this prop, if anyone would like to create a pull request (i'm too lazy) to implement it natively.
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean,
loading?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, loading = false, children, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
disabled={loading}
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
>
{loading && <Loader2 className={`${size !== 'icon' && 'mr-2'} h-4 w-4 animate-spin`} />}
{loading ? size === 'icon' ? '' : children : children}
</Comp>
)
}
)
Affected component/components
Button
Additional Context
No response
Before submitting
- [X] I've made research efforts and searched the documentation
- [X] I've searched for existing issues and PRs
a solution that works with the asChild prop
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ loading = false, children, className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
ref={ref}
disabled={loading}
className={cn(buttonVariants({ variant, size, className }), loading && 'text-transparent')}
{...props}
>
{loading && (
<Icon
icon="svg-spinners:ring-resize"
className="absolute left-1/2 top-1/2 aspect-square h-1/2 -translate-x-1/2 -translate-y-1/2 text-muted"
/>
)}
<Slottable>{children}</Slottable>
</Comp>
)
}
)
- i used
iconifyicons but you can use any other icon you prefer - this solution retains the original width of the button (won't cause layout shift).
- using
Slottablei was able to make it work with theasChildprop requiring only one child.
A better LoadingButton component based on the shadcn-ui Button component. It supports Button component's all props.
import { Loader2 } from "lucide-react";
import { useFormStatus } from "react-dom";
import { Button, type ButtonProps } from "@/components/ui/button";
import { forwardRef } from "react";
type LoadingButtonProps = ButtonProps & {
loading?: boolean;
};
const LoadingButton = forwardRef<HTMLButtonElement, LoadingButtonProps>(
function LoadingButton({ loading, children, ...props }, ref) {
const { pending } = useFormStatus();
const isLoading = loading ?? pending;
return (
<Button ref={ref} disabled={isLoading} {...props}>
<>
{isLoading ? "Please wait" : children}
{isLoading && <Loader2 className="ml-2 h-4 w-4 animate-spin" />}
</>
</Button>
);
},
);
LoadingButton.displayName = "LoadingButton";
export { LoadingButton };
a solution that works with the
asChildpropconst Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ loading = false, children, className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button' return ( <Comp ref={ref} disabled={loading} className={cn(buttonVariants({ variant, size, className }), loading && 'text-transparent')} {...props} > {loading && ( <Icon icon="svg-spinners:ring-resize" className="absolute left-1/2 top-1/2 aspect-square h-1/2 -translate-x-1/2 -translate-y-1/2 text-muted" /> )} <Slottable>{children}</Slottable> </Comp> ) } )
- i used
iconifyicons but you can use any other icon you prefer- this solution retains the original width of the button (won't cause layout shift).
- using
Slottablei was able to make it work with theasChildprop requiring only one child.
works, Thanks for the solution.
The change I did is below...
// components/ui/button.tsx
import * as React from "react";
import { Slot, Slottable } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
white:
"bg-accent text-accent-foreground hover:opacity-90 hover:text-slate-700",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
full: "w-full py-2",
},
rounded: {
default: "rounded-md",
full: "rounded-full",
md: "rounded-md",
lg: "rounded-lg",
},
},
defaultVariants: {
variant: "default",
size: "default",
rounded: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
loading = false,
children,
disabled,
variant,
size,
asChild = false,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
disabled={loading || disabled}
{...props}
>
{loading && (
<Loader2 className="h-5 w-5 mr-2 animate-spin text-muted" />
)}
<Slottable>{loading ? "Loading..." : children}</Slottable>
</Comp>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };
This issue has been automatically closed because it received no activity for a while. If you think it was closed by accident, please leave a comment. Thank you.
Really hope for a first-class support for loading props.
I think it's still not stale though there's some community solution @shadcn
BTW, my modified Button component for New York style:
import { ReloadIcon } from "@radix-ui/react-icons";
import { Slot } from "@radix-ui/react-slot";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import { cn } from "~components/ui/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, disabled, loading, children, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
disabled={loading || disabled}
{...props}
>
{loading && <ReloadIcon className="mr-2 h-4 w-4 animate-spin" />}
{children}
</Comp>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
My version for New York style:
import * as React from "react";
import { Slot, Slottable } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "~/utils/cn";
import { Loader2Icon } from "lucide-react";
const buttonVariants = cva(
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
outline:
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg:not(.loading)]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg:not(.loading)]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg:not(.loading)]:px-4",
icon: "size-9",
},
loading: {
true: "text-transparent",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
function Button({
className,
variant,
size,
children,
disabled,
asChild = false,
loading = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
loading?: boolean;
}) {
const Comp = asChild ? Slot : "button";
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className, loading }))}
disabled={disabled || loading}
{...props}
>
{loading && (
<Loader2Icon
className={cn(
"text-muted absolute animate-spin",
// Used for conditional styling when button is loading
"loading",
)}
/>
)}
<Slottable>{children}</Slottable>
</Comp>
);
}
export { Button, buttonVariants };
- Uses Lucide icon
- Retains the original width of the button (no layout shift)