Shadcn buttons for Deno's Fresh. Shadcn is based on radix-ui and it is not compatible with preact based frontend framework. So I have created button component whic is compatible with preact based framework.
-
Install Fresh by running the following command:
deno run -A -r https://fresh.deno.dev
-
Install the necessary dependencies by running the following command:
deno add jsr:@preact-icons/rx npm:class-variance-authority npm:clsx npm:tailwind-merge npm:tailwindcss-animate
-
Configure tailwind.config.ts file.
import * as animate from "tailwindcss-animate"; import type { Config } from "tailwindcss"; const config: Config = { darkMode: ["class"], content: [ "{routes,islands,components}/**/*.{ts,tsx,js,jsx}", ], theme: { container: { center: true, padding: "2rem", screens: { "2xl": "1400px", }, }, extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, destructive: { DEFAULT: "hsl(var(--destructive))", foreground: "hsl(var(--destructive-foreground))", }, muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))", }, accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))", }, popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))", }, card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, }, borderRadius: { lg: `var(--radius)`, md: `calc(var(--radius) - 2px)`, sm: "calc(var(--radius) - 4px)", }, keyframes: { rainbow: { "0%": { "background-position": "0%" }, "100%": { "background-position": "200%" }, }, "shine": { from: { backgroundPosition: "200% 0" }, to: { backgroundPosition: "-200% 0" }, }, "accordion-down": { from: { height: "0" }, to: { height: "var(--radix-accordion-content-height)" }, }, "accordion-up": { from: { height: "var(--radix-accordion-content-height)", }, to: { height: "0" }, }, buttonheartbeat: { "0%": { "box-shadow": "0 0 0 0 hsl(var(--pulse-color))", }, "50%": { "box-shadow": "0 0 0 7px hsl(var(--pulse-color)/0)", }, "100%": { "box-shadow": "0 0 0 0 hsl(var(--pulse-color)/0)", }, }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "shine": "shine 8s ease-in-out infinite", "accordion-up": "accordion-up 0.2s ease-out", rainbow: "rainbow var(--speed, 2s) infinite linear", buttonheartbeat: "buttonheartbeat 2s infinite ease-in-out", }, }, }, plugins: [ animate, ], }; export default config;
-
Replace the given modules in deno.json file.
"imports": { // other imports // Remove these two lines "preact": "https://esm.sh/[email protected]", "preact/": "https://esm.sh/[email protected]/", // Add these lines insted "preact": "npm:[email protected]", "preact/hooks": "npm:[email protected]/hooks", "preact/jsx-runtime": "npm:[email protected]/jsx-runtime", }, /// Rest of the file
-
Make sure to remove
node_modules
and install the dependencies usingdeno install
command. -
Configure the static/style.css file.
@tailwind base; @tailwind components; @tailwind utilities; @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 47.4% 11.2%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --card: 0 0% 100%; --card-foreground: 222.2 47.4% 11.2%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 100% 50%; --destructive-foreground: 210 40% 98%; --ring: 215 20.2% 65.1%; --radius: 0.5rem; --color-1: 0 100% 63%; --color-2: 270 100% 63%; --color-3: 210 100% 63%; --color-4: 195 100% 63%; --color-5: 90 100% 63%; --pulse-color: var(--primary); } .dark { --background: 224 71% 4%; --foreground: 213 31% 91%; --muted: 223 47% 11%; --muted-foreground: 215.4 16.3% 56.9%; --accent: 216 34% 17%; --accent-foreground: 210 40% 98%; --popover: 224 71% 4%; --popover-foreground: 215 20.2% 65.1%; --border: 216 34% 17%; --input: 216 34% 17%; --card: 224 71% 4%; --card-foreground: 213 31% 91%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 1.2%; --secondary: 222.2 47.4% 11.2%; --secondary-foreground: 210 40% 98%; --destructive: 0 63% 31%; --destructive-foreground: 210 40% 98%; --ring: 216 34% 17%; --radius: 0.5rem; --color-1: 0 100% 63%; --color-2: 270 100% 63%; --color-3: 210 100% 63%; --color-4: 195 100% 63%; --color-5: 90 100% 63%; --pulse-color: var(--primary); } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; font-feature-settings: "rlig" 1, "calt" 1; } }
-
Create
lib/utils.ts
file.import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
-
Create
components/Button.tsx
file.import { ComponentChildren, isValidElement, JSX } from "preact"; import { cloneElement, forwardRef } from "preact/compat"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "../lib/utils.ts"; 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", link: "text-primary underline-offset-4 hover:underline", expandIcon: "group relative text-primary-foreground bg-primary hover:bg-primary/90", ringHover: "bg-primary text-primary-foreground transition-all duration-300 hover:bg-primary/90 hover:ring-2 hover:ring-primary/90 hover:ring-offset-2", shine: "text-primary-foreground animate-shine bg-gradient-to-r from-primary via-primary/75 to-primary bg-[length:400%_100%] ", gooeyRight: "text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 before:absolute before:inset-0 before:-z-10 before:translate-x-[150%] before:translate-y-[150%] before:scale-[2.5] before:rounded-[100%] before:bg-gradient-to-r from-zinc-400 before:transition-transform before:duration-1000 hover:before:translate-x-[0%] hover:before:translate-y-[0%] ", gooeyLeft: "text-primary-foreground relative bg-primary z-0 overflow-hidden transition-all duration-500 after:absolute after:inset-0 after:-z-10 after:translate-x-[-150%] after:translate-y-[150%] after:scale-[2.5] after:rounded-[100%] after:bg-gradient-to-l from-zinc-400 after:transition-transform after:duration-1000 hover:after:translate-x-[0%] hover:after:translate-y-[0%] ", linkHover1: "relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-left after:scale-x-100 hover:after:origin-bottom-right hover:after:scale-x-0 after:transition-transform after:ease-in-out after:duration-300", linkHover2: "relative after:absolute after:bg-primary after:bottom-2 after:h-[1px] after:w-2/3 after:origin-bottom-right after:scale-x-0 hover:after:origin-bottom-left hover:after:scale-x-100 after:transition-transform after:ease-in-out after:duration-300", rainbow: "group relative animate-rainbow cursor-pointer border-0 bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] bg-[length:200%] text-primary-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.08*1rem)_solid_transparent] before:absolute before:bottom-[-20%] before:left-1/2 before:z-[0] before:h-[20%] before:w-[60%] before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] before:bg-[length:200%] before:[filter:blur(calc(0.8*1rem))] dark:bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))]", "rainbow-outline": "group relative animate-rainbow cursor-pointer border-0 border-input bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] bg-[length:200%] px-4 text-foreground shadow-sm [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.08*1rem)_solid_transparent] before:absolute before:bottom-[-20%] before:left-1/2 before:z-[0] before:h-[20%] before:w-[60%] before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] before:bg-[length:200%] before:[filter:blur(calc(0.8*1rem))] dark:bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))]", hearbeat: "animate-buttonheartbeat bg-primary text-primary-foreground", deno1: "bg-gradient-to-br from-primary/40 to-primary text-white font-semibold hover:bg-secondary transition-colors duration-200 ease-in-out", deno2: "border-[1.5px] border-primary bg-secondary font-bold text-primary shadow-[5px_6px_0_0_#64748b55] outline-primary outline-offset-2 transition-all duration-75 ease-in-out rounded-lg hover:bg-primary/5 focus:outline focus:outline-2", "smooth-bounce": "transition-transform duration-300 ease-in-out hover:scale-110 bg-primary text-primary-foreground", }, 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", }, }, defaultVariants: { variant: "default", size: "default", }, }, ); interface IconProps { Icon: preact.ComponentType; iconPlacement: "left" | "right"; } interface IconRefProps { Icon?: never; iconPlacement?: undefined; } type ButtonHTMLAttributes = Omit< JSX.HTMLAttributes<HTMLButtonElement>, keyof VariantProps<typeof buttonVariants> >; export interface ButtonProps extends ButtonHTMLAttributes, VariantProps<typeof buttonVariants> { asChild?: boolean; children?: ComponentChildren; pulseColor?: string; } export type ButtonIconProps = IconProps | IconRefProps; const Button = forwardRef< HTMLButtonElement, ButtonProps & ButtonIconProps >( ( { className, variant, asChild = false, size, Icon, iconPlacement, children, ...props }, ref, ) => { if (asChild && isValidElement(children)) { return cloneElement(children as preact.VNode, { className: cn(buttonVariants({ variant, size, className })), ref, ...props, children: ( <> {Icon && iconPlacement === "left" && ( <div className="w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-100 group-hover:pr-2 group-hover:opacity-100"> <Icon /> </div> )} {children.props.children} {Icon && iconPlacement === "right" && ( <div className="w-0 translate-x-[100%] pl-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-0 group-hover:pl-2 group-hover:opacity-100"> <Icon /> </div> )} </> ), }); } return ( <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} > {Icon && iconPlacement === "left" && ( <div className="w-0 translate-x-[0%] pr-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-100 group-hover:pr-2 group-hover:opacity-100"> <Icon /> </div> )} {children} {Icon && iconPlacement === "right" && ( <div className="w-0 translate-x-[100%] pl-0 opacity-0 transition-all duration-200 group-hover:w-5 group-hover:translate-x-0 group-hover:pl-2 group-hover:opacity-100"> <Icon /> </div> )} </button> ); }, ); Button.displayName = "Button"; export { Button, buttonVariants };
- That's all you have to do. Now you can check the examples and copy their code snippets. Also, check out the props, as they are mostly the same as Shadcn buttons.
Prop | Type | Default | Values | Description |
---|---|---|---|---|
variant | string | default | default | destructive | outline | secondary | ghost | link | expandIcon | ringHover | shine | gooeyRight | gooeyLeft | linkHover1 | linkHover2 | rainbow | rainbow-outline | hearbeat | deno1 | deno2 | smooth-bounce | Type of the button. |
size | string | default | default | sm | lg | icon | Size of the button. |
className | string | - | - | Custom CSS class for styling. |
children | Preact.ComponentChildren | - | - | Children elements. |
asChild | boolean | false | true | false | Make it Enable to pass all props of button to its immediate child. |
Icon | Preact.ComponentType | - | Any icon from Icon libraries | Use it with variant 'expandIcon' and specify iconPlacement. |
iconPlacement | string | - | left | right | Use it with variant 'expandIcon' and specify Icon. |
- Special thanks to Shadcn, enhanced-button, Magic UI and Syntax UI for the inspiration.
Support me by giving a star ⭐ on this repository.
This project is licensed under the MIT License.