Accessible button with variants
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
function ArrowUpIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 7-7 7 7M12 19V5" /></svg>
}
export default function ButtonDemoExample() {
return (
<div class="flex flex-wrap items-center gap-2">
<Button variant="outline">Button</Button>
<Button variant="outline" size="icon" aria-label="Submit">
<ArrowUpIcon />
</Button>
</div>
)
}
Installation
npx @fictjs/shadcn@latest add buttonInstall the runtime dependencies:
npm install @fictjs/radix-ui class-variance-authorityCopy the Fict shadcn component into your project.
Source
components/ui/button.tsx
src/components/ui/button.tsx
import { Slot } from '@fictjs/radix-ui'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/cn'
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring',
{
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',
xs: 'h-6 rounded-md px-2 text-xs',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
'icon-xs': 'h-6 w-6 rounded-md',
'icon-sm': 'h-8 w-8 rounded-md',
'icon-lg': 'h-10 w-10 rounded-md',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
)
type ButtonProps = JSX.IntrinsicElements['button'] & VariantProps<typeof buttonVariants> & {
asChild?: boolean
}
export function Button(props: ButtonProps) {
const { asChild, class: className, variant, size, ...rest } = props
const classValue = cn(buttonVariants({ variant, size }), className)
if (asChild) {
return <Slot.Root class={classValue} data-slot='button' data-variant={variant} data-size={size} {...rest} />
}
return <button class={classValue} data-slot='button' data-variant={variant} data-size={size} {...rest} />
}
export { buttonVariants }
Usage
Examples
Size
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
function ArrowUpRightIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 17 17 7M7 7h10v10" /></svg>
}
export default function ButtonSizeExample() {
return (
<div class="flex flex-col items-start gap-8 sm:flex-row">
<div class="flex items-start gap-2">
<Button variant="outline" size="xs">Extra Small</Button>
<Button variant="outline" size="icon-xs" aria-label="Extra small submit"><ArrowUpRightIcon /></Button>
</div>
<div class="flex items-start gap-2">
<Button variant="outline" size="sm">Small</Button>
<Button variant="outline" size="icon-sm" aria-label="Small submit"><ArrowUpRightIcon /></Button>
</div>
<div class="flex items-start gap-2">
<Button variant="outline">Default</Button>
<Button variant="outline" size="icon" aria-label="Default submit"><ArrowUpRightIcon /></Button>
</div>
<div class="flex items-start gap-2">
<Button variant="outline" size="lg">Large</Button>
<Button variant="outline" size="icon-lg" aria-label="Large submit"><ArrowUpRightIcon /></Button>
</div>
</div>
)
}
Default
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
export default function ButtonDefaultExample() {
return <Button>Button</Button>
}
Outline
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
export default function ButtonOutlineExample() {
return <Button variant="outline">Outline</Button>
}
Secondary
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
export default function ButtonSecondaryExample() {
return <Button variant="secondary">Secondary</Button>
}
Ghost
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
export default function ButtonGhostExample() {
return <Button variant="ghost">Ghost</Button>
}
Destructive
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
export default function ButtonDestructiveExample() {
return <Button variant="destructive">Destructive</Button>
}
Link
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
export default function ButtonLinkExample() {
return <Button variant="link">Link</Button>
}
Icon
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
function CircleArrowIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="m8 13 4-4 4 4M12 9v6" /></svg>
}
export default function ButtonIconExample() {
return <Button variant="outline" size="icon" aria-label="Submit"><CircleArrowIcon /></Button>
}
With Icon
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
function BranchIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="5" r="2" /><circle cx="18" cy="6" r="2" /><circle cx="6" cy="19" r="2" /><path d="M6 7v10M8 7c4 0 4 5 8 5" /></svg>
}
export default function ButtonWithIconExample() {
return <Button variant="outline" size="sm"><BranchIcon />New Branch</Button>
}
Rounded
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
function ArrowUpIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 7-7 7 7M12 19V5" /></svg>
}
export default function ButtonRoundedExample() {
return <Button variant="outline" size="icon" class="rounded-full" aria-label="Submit"><ArrowUpIcon /></Button>
}
Spinner
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
...import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
export default function ButtonSpinnerExample() {
return (
<div class="flex gap-2">
<Button variant="outline" disabled><Spinner />Generating</Button>
<Button variant="secondary" disabled>Downloading<Spinner /></Button>
</div>
)
}
Group Demo
import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
...import { Button } from '@/components/ui/button'
import { ButtonGroup } from '@/components/ui/button-group'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuRadio,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
function MoreIcon() {
return <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="5" cy="12" r="1" /><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /></svg>
}
export default function ButtonGroupDemoExample() {
let label = $state('personal')
return (
<div class="flex flex-wrap gap-2">
<ButtonGroup><Button variant="outline" size="icon" aria-label="Go back">←</Button></ButtonGroup>
<ButtonGroup><Button variant="outline">Archive</Button><Button variant="outline">Report</Button></ButtonGroup>
<ButtonGroup>
<Button variant="outline">Snooze</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild><Button variant="outline" size="icon" aria-label="More options"><MoreIcon /></Button></DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Mark as Read</DropdownMenuItem>
<DropdownMenuItem>Archive</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Snooze</DropdownMenuItem>
<DropdownMenuItem>Add to Calendar</DropdownMenuItem>
<DropdownMenuItem>Add to List</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Label As...</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadio value={label} onValueChange={value => { label = String(value) }}>
<DropdownMenuRadioItem value="personal">Personal</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="work">Work</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="other">Other</DropdownMenuRadioItem>
</DropdownMenuRadio>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem>Trash</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</div>
)
}
Aschild
import { Button } from '@/components/ui/button'
...import { Button } from '@/components/ui/button'
export default function ButtonAsChildExample() {
return <Button asChild><a href="/login">Login</a></Button>
}
Rtl
import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
...import { Button } from '@/components/ui/button'
import { Spinner } from '@/components/ui/spinner'
const translations = {
ar: { button: 'زر', delete: 'حذف', submit: 'إرسال', loading: 'جاري التحميل' },
he: { button: 'כפתור', delete: 'מחק', submit: 'שלח', loading: 'טוען' },
en: { button: 'Button', delete: 'Delete', submit: 'Submit', loading: 'Loading' },
} as const
export default function ButtonRtlExample() {
let language = $state<keyof typeof translations>('ar')
const text = () => translations[language]
const direction = () => language === 'en' ? 'ltr' : 'rtl'
return (
<div class="grid gap-4">
<select value={language} onChange={event => { language = event.currentTarget.value as keyof typeof translations }}>
<option value="ar">Arabic (العربية)</option>
<option value="he">Hebrew (עברית)</option>
<option value="en">English</option>
</select>
<div class="flex flex-wrap items-center gap-2" dir={direction()}>
<Button variant="outline">{text().button}</Button>
<Button variant="destructive">{text().delete}</Button>
<Button variant="outline">{text().submit} →</Button>
<Button variant="outline" size="icon" aria-label="Add">+</Button>
<Button variant="secondary" disabled><Spinner />{text().loading}</Button>
</div>
</div>
)
}