FluxUI Pro is live - modern UI, powerful animations, zero hassle.
Components
Checkbox

Checkbox

Ultra-modern checkbox component with custom animations, multiple variants, and enhanced user interaction for form controls.

Basic States

Basic Checkbox

Status: Unchecked

With Description

Error State

Disabled State

Variants

Default Variant

Filled Variant

Outlined Variant

Sizes

Indeterminate State

Interactive Form Demo

Account Preferences

Keyboard Navigation

Try these keyboard shortcuts:

  • Tab: Move focus to checkbox
  • Space/Enter: Toggle checkbox
  • Arrow Keys: Navigate between options

Installation

1

Install the packages

npm i motion clsx tailwind-merge lucide-react
2

Add util file

lib/util.ts
import { ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
3

Copy and paste the following code into your project

checkbox.tsx
"use client"; import { motion, AnimatePresence } from "motion/react"; import React, { useState, useId } from "react"; import { cn } from "@/lib/utils"; import { Check, Minus, AlertCircle } from "lucide-react"; interface CheckboxProps { checked?: boolean; onChange?: (checked: boolean) => void; label?: string; description?: string; disabled?: boolean; required?: boolean; error?: string; size?: "sm" | "md" | "lg"; variant?: "default" | "filled" | "outlined"; indeterminate?: boolean; className?: string; } const Checkbox: React.FC<CheckboxProps> = ({ checked = false, onChange, label, description, disabled = false, required = false, error, size = "md", variant = "default", indeterminate = false, className, }) => { const [isFocused, setIsFocused] = useState(false); const checkboxId = useId(); const hasError = !!error; const sizeClasses = { sm: "w-4 h-4", md: "w-5 h-5", lg: "w-6 h-6", }; const iconSizeClasses = { sm: "w-3 h-3", md: "w-4 h-4", lg: "w-5 h-5", }; const textSizeClasses = { sm: "text-sm", md: "text-base", lg: "text-lg", }; const variantClasses = { default: "border-gray-300 bg-white hover:border-gray-400 focus:border-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:hover:border-gray-500", filled: "border-transparent bg-gray-100 hover:bg-gray-200 focus:bg-white focus:border-blue-500 dark:bg-gray-700 dark:hover:bg-gray-600", outlined: "border-2 border-gray-300 bg-transparent hover:border-gray-400 focus:border-blue-500 dark:border-gray-600", }; const checkedClasses = { default: "bg-blue-500 border-blue-500 text-white dark:bg-blue-600 dark:border-blue-600", filled: "bg-blue-500 border-blue-500 text-white dark:bg-blue-600 dark:border-blue-600", outlined: "bg-blue-500 border-blue-500 text-white dark:bg-blue-600 dark:border-blue-600", }; const handleClick = () => { if (!disabled) { onChange?.(!checked); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleClick(); } }; return ( <div className={cn("relative", className)}> <div className="flex items-start space-x-3"> {/* Checkbox Input */} <div className="relative flex-shrink-0"> <motion.input id={checkboxId} type="checkbox" checked={checked} onChange={() => {}} // Handled by click disabled={disabled} className="sr-only" /> {/* Custom Checkbox */} <motion.div className={cn( "relative rounded-md border-2 cursor-pointer transition-all duration-200 flex items-center justify-center", sizeClasses[size], checked ? checkedClasses[variant] : variantClasses[variant], disabled && "opacity-50 cursor-not-allowed", hasError && !checked && "border-red-500", isFocused && "ring-2 ring-blue-500/20" )} onClick={handleClick} onKeyDown={handleKeyDown} tabIndex={disabled ? -1 : 0} role="checkbox" aria-checked={indeterminate ? "mixed" : checked} aria-labelledby={label ? checkboxId : undefined} animate={{ scale: isFocused ? 1.05 : 1, boxShadow: isFocused ? "0 0 0 3px rgba(59, 130, 246, 0.1)" : "0 0 0 0px rgba(0, 0, 0, 0)", }} transition={{ duration: 0.2 }} whileHover={!disabled ? { scale: 1.05 } : {}} whileTap={!disabled ? { scale: 0.95 } : {}} > {/* Background Animation */} <AnimatePresence> {(checked || indeterminate) && ( <motion.div className="absolute inset-0 bg-current rounded-md" initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} transition={{ duration: 0.2 }} /> )} </AnimatePresence> {/* Check Icon */} <AnimatePresence mode="wait"> {checked && !indeterminate && ( <motion.div key="check" initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} transition={{ duration: 0.2, delay: 0.1 }} > <Check className={cn("text-white", iconSizeClasses[size])} /> </motion.div> )} {indeterminate && ( <motion.div key="minus" initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0, opacity: 0 }} transition={{ duration: 0.2, delay: 0.1 }} > <Minus className={cn("text-white", iconSizeClasses[size])} /> </motion.div> )} </AnimatePresence> {/* Ripple Effect */} <AnimatePresence> {isFocused && ( <motion.div className="absolute inset-0 bg-white/30 rounded-md" initial={{ scale: 0, opacity: 1 }} animate={{ scale: 2, opacity: 0 }} exit={{ scale: 0, opacity: 0 }} transition={{ duration: 0.4 }} /> )} </AnimatePresence> </motion.div> {/* Focus Ring */} <AnimatePresence> {isFocused && ( <motion.div className="absolute inset-0 rounded-md border-2 border-blue-500" initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.8 }} transition={{ duration: 0.2 }} /> )} </AnimatePresence> </div> {/* Label and Description */} <div className="flex-1 min-w-0"> {label && ( <motion.label htmlFor={checkboxId} className={cn( "block font-medium cursor-pointer transition-colors duration-200", textSizeClasses[size], hasError ? "text-red-600 dark:text-red-400" : disabled ? "text-gray-400 dark:text-gray-500" : "text-gray-900 dark:text-white hover:text-gray-700 dark:hover:text-gray-200" )} onClick={handleClick} animate={{ x: isFocused ? 2 : 0, }} transition={{ duration: 0.2 }} > {label} {required && <span className="text-red-500 ml-1">*</span>} </motion.label> )} {description && ( <motion.p className={cn( "mt-1 text-sm transition-colors duration-200", disabled ? "text-gray-400 dark:text-gray-500" : "text-gray-600 dark:text-gray-400" )} animate={{ x: isFocused ? 2 : 0, }} transition={{ duration: 0.2, delay: 0.05 }} > {description} </motion.p> )} {/* Error Message */} <AnimatePresence> {hasError && ( <motion.div initial={{ opacity: 0, y: -10, height: 0 }} animate={{ opacity: 1, y: 0, height: "auto" }} exit={{ opacity: 0, y: -10, height: 0 }} transition={{ duration: 0.2 }} className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1" > <AlertCircle className="w-4 h-4 flex-shrink-0" /> {error} </motion.div> )} </AnimatePresence> </div> </div> </div> ); }; export default Checkbox;
4

Update the import paths to match your project setup

Props

PropTypeDefaultDescription
checkedbooleanfalseWhether the checkbox is checked.
onChange(checked: boolean) => void() => {}Callback function when checkbox state changes.
labelstringundefinedLabel text for the checkbox.
descriptionstringundefinedDescription text below the label.
disabledbooleanfalseDisable the checkbox interaction.
requiredbooleanfalseMark checkbox as required.
errorstringundefinedError message to display.
size'sm' | 'md' | 'lg''md'Size of the checkbox.
variant'default' | 'filled' | 'outlined''default'Visual style variant.
indeterminatebooleanfalseShow indeterminate state (dash instead of check).