# KokonutUI - Full Documentation
> Collection of 100+ stunning UI components free and open source built with Next.js, React, Tailwind CSS, and Motion.
Every documentation page, concatenated. Index: https://kokonutui.com/llms.txt
---
# Installation
> Install KokonutUI components in any React or Next.js project with the shadcn CLI, using namespaces or direct registry URLs. No extra setup required.
Source: https://kokonutui.com/docs
### 1. Add namespaces with components.json [#1-add-namespaces-with-componentsjson]
The components.json file holds configuration for your project, and allow easy installation of any components.
It is only required if you're using the CLI to add components to your project. If you're using the copy and paste method, you don't need this file.
You can create a components.json file in your project by running the following command:
```bash
bunx --bun shadcn@latest init
```
```bash
npx shadcn@latest init
```
```bash
pnpm dlx shadcn@latest init
```
Then, you'll need to add this to your `components.json` to allow kokonutUI registry.
```json
{
"registries": {
"@kokonutui": "https://kokonutui.com/r/{name}.json"
}
}
```
### 2. Install utilities [#2-install-utilities]
All components use [Tailwind CSS v4](https://tailwindcss.com/docs/installation/framework-guides/nextjs), so ensure it's installed in your project.
Many components also use the `cn` utility functionβinstall it with the following command:
```bash
bunx --bun shadcn@latest add https://kokonutui.com/r/utils.json
```
```bash
npx shadcn@latest add https://kokonutui.com/r/utils.json
```
```bash
pnpm dlx shadcn@latest add https://kokonutui.com/r/utils.json
```
### 3. That's it π [#3-thats-it-]
We use [lucide-icons](https://lucide.dev/guide/installation) for most components that include icons, along with some [shadcn/ui](https://ui.shadcn.com/) components. These dependencies will be automatically installed when using the CLI.
For exemple to add `particle-button` component to your project, it will be easy as:
```bash
bunx --bun shadcn@latest add @kokonutui/particle-button
```
```bash
npx shadcn@latest add @kokonutui/particle-button
```
```bash
pnpm dlx shadcn@latest add @kokonutui/particle-button
```
*While we provide a convenient 'copy' button for the code, we strongly recommend using the CLI for installing components, as it ensures all necessary files are included.*
Add to your page and it works!
```jsx
import ParticleButton from "@/components/kokonutui/particle-button";
export default function Page() {
return ;
}
```
### 4. Using an AI assistant? [#4-using-an-ai-assistant]
KokonutUI works with the shadcn MCP server, so Claude Code, Cursor, VS Code, and Codex can browse and install components through natural language. See the [MCP Server guide](/docs/mcp) to set it up.
### 5. Optionnal dependencies [#5-optionnal-dependencies]
Some components require additional libraries, listed at the bottom of each components. Make sure to install them to ensure the component works properly.
### 6. Monorepo [#6-monorepo]
For monorepo `shadcn/ui` CLI contain the options `-c` to the path to your workspace for exemple:
```bash
bunx --bun shadcn@latest add @kokonutui/particle-button -c ./apps/www
```
```bash
npx shadcn@latest add @kokonutui/particle-button -c ./apps/www
```
```bash
pnpm dlx shadcn@latest add @kokonutui/particle-button -c ./apps/www
```
---
# MCP Server
> Browse, search, and install KokonutUI components from Claude Code, Cursor, VS Code, or Codex using natural language via the shadcn MCP server.
Source: https://kokonutui.com/docs/mcp
KokonutUI works out of the box with the [shadcn MCP server](https://ui.shadcn.com/docs/mcp). Once configured, your AI assistant can list every KokonutUI component, read its dependencies, and install it into your project β no manual CLI commands needed.
```text
"Add @kokonutui/particle-button to my project"
```
### 1. Add the KokonutUI registry [#1-add-the-kokonutui-registry]
The MCP server discovers registries through your `components.json`. Make sure the `@kokonutui` namespace is configured β see the [installation guide](/docs) if you haven't set it up yet:
```json
{
"registries": {
"@kokonutui": "https://kokonutui.com/r/{name}.json"
}
}
```
### 2. Set up the MCP server [#2-set-up-the-mcp-server]
The fastest way is the `mcp init` command, which configures the server for your client automatically:
```bash
bunx --bun shadcn@latest mcp init --client claude
```
```bash
npx shadcn@latest mcp init --client claude
```
```bash
pnpm dlx shadcn@latest mcp init --client claude
```
Use `--client claude`, `--client cursor`, `--client vscode`, or `--client codex` depending on your editor.
You can also configure it manually:
```json title=".mcp.json"
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}
```
```json title=".cursor/mcp.json"
{
"mcpServers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}
```
```json title=".vscode/mcp.json"
{
"servers": {
"shadcn": {
"command": "npx",
"args": ["shadcn@latest", "mcp"]
}
}
}
```
```toml title="~/.codex/config.toml"
[mcp_servers.shadcn]
command = "npx"
args = ["shadcn@latest", "mcp"]
```
For Cursor, enable the server in **Settings β MCP** after adding the file. For VS Code, click **Start** next to the server entry in `.vscode/mcp.json`.
### 3. Use it [#3-use-it]
Restart your editor or MCP client, then ask in plain language:
```text
"Show me all available components in the @kokonutui registry"
```
```text
"Add @kokonutui/ai-prompt to my project"
```
```text
"Build a pricing section using @kokonutui components"
```
The assistant resolves each component's `registryDependencies` (shadcn/ui primitives like `button` or `textarea`) and npm `dependencies` (like `motion` or `lucide-react`) automatically, exactly like the CLI does.
The shadcn MCP server follows the standard Model Context Protocol, so any MCP-capable client can use it β the four above are just the ones with first-class `mcp init` support.
---
# AI Input Search
> AI search chat input with toggleable web search mode, file attachment and auto-resizing textarea. Built with React, Tailwind CSS and Motion.
Source: https://kokonutui.com/docs/ai/ai-input-search
## Installation
```bash
npx shadcn@latest add @kokonutui/ai-input-search
```
## Source
```tsx
"use client";
/**
* @author: @kokonutui
* @description: AI Input Search
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { Globe, Paperclip, Send } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { Textarea } from "@/components/ui/textarea";
import { useAutoResizeTextarea } from "@/hooks/use-auto-resize-textarea";
import { cn } from "@/lib/utils";
interface AIInputSearchProps {
placeholder?: string;
searchLabel?: string;
onSubmit?: (value: string) => void;
className?: string;
}
export default function AI_Input_Search({
placeholder = "Search the web...",
searchLabel = "Search",
onSubmit,
className,
}: AIInputSearchProps) {
const [value, setValue] = useState("");
const { textareaRef, adjustHeight } = useAutoResizeTextarea({
minHeight: 52,
maxHeight: 200,
});
const [showSearch, setShowSearch] = useState(true);
const [isFocused, setIsFocused] = useState(false);
const handleSubmit = () => {
onSubmit?.(value);
setValue("");
adjustHeight(true);
};
const handleFocus = () => {
setIsFocused(true);
};
const handleBlur = () => {
setIsFocused(false);
};
const handleContainerClick = () => {
if (textareaRef.current) {
textareaRef.current.focus();
}
};
return (
{
if (e.key === "Enter" || e.key === " ") {
handleContainerClick();
}
}}
role="textbox"
tabIndex={0}
>
);
}
```
---
# AI State Loading
> Animated AI loading state cycling task statuses through a scrolling code-style log with an SVG progress spinner. Built with React and Tailwind CSS.
Source: https://kokonutui.com/docs/ai/ai-loading
## Installation
```bash
npx shadcn@latest add @kokonutui/ai-loading
```
## Source
```tsx
"use client";
/**
* @author: @kokonutui
* @description: AI Loading State
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { useEffect, useRef, useState } from "react";
const TASK_SEQUENCES = [
{
status: "Searching the web",
lines: [
"Initializing web search...",
"Scanning web pages...",
"Visiting 5 websites...",
"Analyzing content...",
"Generating summary...",
],
},
{
status: "Analyzing results",
lines: [
"Analyzing search results...",
"Generating summary...",
"Checking for relevant information...",
"Finalizing analysis...",
"Setting up lazy loading...",
"Configuring caching strategies...",
"Running performance tests...",
"Finalizing optimizations...",
],
},
{
status: "Enhancing UI/UX",
lines: [
"Initializing UI enhancement scan...",
"Checking accessibility compliance...",
"Analyzing component animations...",
"Reviewing loading states...",
"Testing responsive layouts...",
"Optimizing user interactions...",
"Validating color contrast...",
"Checking motion preferences...",
"Finalizing UI improvements...",
],
},
];
const LoadingAnimation = ({ progress }: { progress: number }) => (
Loading Progress Indicator
);
export default function AILoadingState() {
const [sequenceIndex, setSequenceIndex] = useState(0);
const [visibleLines, setVisibleLines] = useState<
Array<{ text: string; number: number }>
>([]);
const [scrollPosition, setScrollPosition] = useState(0);
const codeContainerRef = useRef(null);
const rootRef = useRef(null);
const [isVisible, setIsVisible] = useState(true);
const lineHeight = 28;
// Only animate while on screen β an off-screen interval keeps waking the
// main thread and re-rendering for a component nobody can see.
useEffect(() => {
const element = rootRef.current;
if (!element) {
return;
}
const observer = new IntersectionObserver(
([entry]) => setIsVisible(entry.isIntersecting),
{ rootMargin: "100px" }
);
observer.observe(element);
return () => observer.disconnect();
}, []);
const currentSequence = TASK_SEQUENCES[sequenceIndex];
const totalLines = currentSequence.lines.length;
useEffect(() => {
const initialLines = [];
for (let i = 0; i < Math.min(5, totalLines); i++) {
initialLines.push({
text: currentSequence.lines[i],
number: i + 1,
});
}
setVisibleLines(initialLines);
setScrollPosition(0);
}, [sequenceIndex, currentSequence.lines, totalLines]);
// Handle line advancement
useEffect(() => {
if (!isVisible) {
return;
}
const advanceTimer = setInterval(() => {
// Get the current first visible line index
const firstVisibleLineIndex = Math.floor(scrollPosition / lineHeight);
const nextLineIndex = (firstVisibleLineIndex + 3) % totalLines;
// If we're about to wrap around, move to next sequence
if (nextLineIndex < firstVisibleLineIndex && nextLineIndex !== 0) {
setSequenceIndex(
(prevIndex) => (prevIndex + 1) % TASK_SEQUENCES.length
);
return;
}
// Add the next line if needed
if (nextLineIndex >= visibleLines.length && nextLineIndex < totalLines) {
setVisibleLines((prevLines) => [
...prevLines,
{
text: currentSequence.lines[nextLineIndex],
number: nextLineIndex + 1,
},
]);
}
// Scroll to the next line
setScrollPosition((prevPosition) => prevPosition + lineHeight);
}, 2000); // Slightly slower than the example for better readability
return () => clearInterval(advanceTimer);
}, [
isVisible,
scrollPosition,
visibleLines,
totalLines,
sequenceIndex,
currentSequence.lines,
lineHeight,
]);
// Apply scroll position
useEffect(() => {
if (codeContainerRef.current) {
codeContainerRef.current.scrollTop = scrollPosition;
}
}, [scrollPosition]);
return (
{currentSequence.status}...
{visibleLines.map((line, index) => (
{line.number}
{line.text}
))}
);
}
```
---
# AI Input Selector
> Animated AI chat input with model selection dropdown, file attachment and auto-resizing textarea. Built with React, Tailwind CSS and Motion.
Source: https://kokonutui.com/docs/ai/ai-prompt
## Installation
```bash
npx shadcn@latest add @kokonutui/ai-prompt
```
## Source
```tsx
"use client";
/**
* @author: @kokonutui
* @description: AI Prompt Input
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { ArrowRight, Bot, Check, ChevronDown, Paperclip } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import Anthropic from "@/components/icons/anthropic";
import AnthropicDark from "@/components/icons/anthropic-dark";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Textarea } from "@/components/ui/textarea";
import { useAutoResizeTextarea } from "@/hooks/use-auto-resize-textarea";
import { cn } from "@/lib/utils";
const OPENAI_SVG = (
OpenAI Icon Light
OpenAI Icon Dark
);
interface AIPromptProps {
models?: string[];
defaultModel?: string;
placeholder?: string;
headerText?: string;
headerAction?: string;
onSubmit?: (value: string, model: string) => void;
className?: string;
}
const DEFAULT_MODELS = [
"Gemini 3 Pro",
"GPT-5.6 Mini",
"Claude Fable 5",
"GPT-5.6 Codex",
"GPT-5.6",
];
export default function AI_Prompt({
models = DEFAULT_MODELS,
defaultModel = "Claude Fable 5",
placeholder = "What can I do for you?",
headerText = "is free this weekend!",
headerAction = "Ship Now!",
onSubmit,
className,
}: AIPromptProps) {
const [value, setValue] = useState("");
const { textareaRef, adjustHeight } = useAutoResizeTextarea({
minHeight: 72,
maxHeight: 300,
});
const [selectedModel, setSelectedModel] = useState(defaultModel);
const MODEL_ICONS: Record = {
"GPT-5.6 Mini": OPENAI_SVG,
"Gemini 3 Pro": (
Gemini
),
"Claude Fable 5": (
Anthropic Icon Light
Anthropic Icon Dark
),
"GPT-5.6 Codex": OPENAI_SVG,
"GPT-5.6": OPENAI_SVG,
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
onSubmit?.(value, selectedModel);
setValue("");
adjustHeight(true);
}
};
return (
);
}
```
---
# AI Text Loading
> Animated AI thinking indicator that cycles status messages with a shimmering gradient text effect. Built with React, Tailwind CSS and Motion.
Source: https://kokonutui.com/docs/ai/ai-text-loading
## Installation
```bash
npx shadcn@latest add @kokonutui/ai-text-loading
```
## Source
```tsx
"use client";
/**
* @author: @kokonutui
* @description: AI Text Loading
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
interface AITextLoadingProps {
texts?: string[];
className?: string;
interval?: number;
}
export default function AITextLoading({
texts = [
"Thinking...",
"Processing...",
"Analyzing...",
"Computing...",
"Almost...",
],
className,
interval = 1500,
}: AITextLoadingProps) {
const [currentTextIndex, setCurrentTextIndex] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCurrentTextIndex((prevIndex) => (prevIndex + 1) % texts.length);
}, interval);
return () => clearInterval(timer);
}, [interval, texts.length]);
return (
{texts[currentTextIndex]}
);
}
```
---
# AI Voice
> AI voice input button with recording timer, pulsing waveform bars and listening state. Built with React and animated Tailwind CSS styles.
Source: https://kokonutui.com/docs/ai/ai-voice
## Installation
```bash
npx shadcn@latest add @kokonutui/ai-voice
```
## Source
```tsx
"use client";
/**
* @author: @kokonutui
* @description: AI Voice
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { Mic } from "lucide-react";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
export default function AI_Voice() {
const [submitted, setSubmitted] = useState(false);
const [time, setTime] = useState(0);
const [isClient, setIsClient] = useState(false);
const [isDemo, setIsDemo] = useState(true);
useEffect(() => {
setIsClient(true);
}, []);
useEffect(() => {
let intervalId: NodeJS.Timeout;
if (submitted) {
intervalId = setInterval(() => {
setTime((t) => t + 1);
}, 1000);
} else {
setTime(0);
}
return () => clearInterval(intervalId);
}, [submitted]);
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, "0")}:${secs
.toString()
.padStart(2, "0")}`;
};
/**
* Remove that, only used for demo
*/
useEffect(() => {
if (!isDemo) return;
let timeoutId: NodeJS.Timeout;
const runAnimation = () => {
setSubmitted(true);
timeoutId = setTimeout(() => {
setSubmitted(false);
timeoutId = setTimeout(runAnimation, 1000);
}, 3000);
};
const initialTimeout = setTimeout(runAnimation, 100);
return () => {
clearTimeout(timeoutId);
clearTimeout(initialTimeout);
};
}, [isDemo]);
const handleClick = () => {
if (isDemo) {
setIsDemo(false);
setSubmitted(false);
} else {
setSubmitted((prev) => !prev);
}
};
return (
{submitted ? (
) : (
)}
{formatTime(time)}
{[...Array(48)].map((_, i) => (
))}
{submitted ? "Listening..." : "Click to speak"}
);
}
```
---
# Background Paths
> Animated background of flowing SVG line paths drawn across the hero section. Built with React, Tailwind CSS and Motion path animations.
Source: https://kokonutui.com/docs/backgrounds/background-paths
## Installation
```bash
npx shadcn@latest add @kokonutui/background-paths
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Background Paths
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { memo, useMemo } from "react";
interface Point {
x: number;
y: number;
}
interface PathData {
id: string;
d: string;
opacity: number;
width: number;
duration: number;
delay: number;
}
// Path generation function
function generateAestheticPath(
index: number,
position: number,
type: "primary" | "secondary" | "accent"
): string {
const baseAmplitude =
type === "primary" ? 150 : type === "secondary" ? 100 : 60;
const phase = index * 0.2;
const points: Point[] = [];
const segments = type === "primary" ? 10 : type === "secondary" ? 8 : 6;
const startX = 2400;
const startY = 800;
const endX = -2400;
const endY = -800 + index * 25;
for (let i = 0; i <= segments; i++) {
const progress = i / segments;
const eased = 1 - (1 - progress) ** 2;
const baseX = startX + (endX - startX) * eased;
const baseY = startY + (endY - startY) * eased;
const amplitudeFactor = 1 - eased * 0.3;
const wave1 =
Math.sin(progress * Math.PI * 3 + phase) *
(baseAmplitude * 0.7 * amplitudeFactor);
const wave2 =
Math.cos(progress * Math.PI * 4 + phase) *
(baseAmplitude * 0.3 * amplitudeFactor);
const wave3 =
Math.sin(progress * Math.PI * 2 + phase) *
(baseAmplitude * 0.2 * amplitudeFactor);
points.push({
x: baseX * position,
y: baseY + wave1 + wave2 + wave3,
});
}
const pathCommands = points.map((point: Point, i: number) => {
if (i === 0) return `M ${point.x} ${point.y}`;
const prevPoint = points[i - 1];
const tension = 0.4;
const cp1x = prevPoint.x + (point.x - prevPoint.x) * tension;
const cp1y = prevPoint.y;
const cp2x = prevPoint.x + (point.x - prevPoint.x) * (1 - tension);
const cp2y = point.y;
return `C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${point.x} ${point.y}`;
});
return pathCommands.join(" ");
}
const generateUniqueId = (prefix: string): string =>
`${prefix}-${Math.random().toString(36).substr(2, 9)}`;
// Memoized FloatingPaths component
const FloatingPaths = memo(function FloatingPaths({
position,
}: {
position: number;
}) {
// Increased number of paths while maintaining optimization
const primaryPaths: PathData[] = useMemo(
() =>
Array.from({ length: 12 }, (_, i) => ({
id: generateUniqueId("primary"),
d: generateAestheticPath(i, position, "primary"),
opacity: 0.15 + i * 0.02,
width: 4 + i * 0.3,
duration: 25,
delay: 0,
})),
[position]
);
const secondaryPaths: PathData[] = useMemo(
() =>
Array.from({ length: 15 }, (_, i) => ({
id: generateUniqueId("secondary"),
d: generateAestheticPath(i, position, "secondary"),
opacity: 0.12 + i * 0.015,
width: 3 + i * 0.25,
duration: 20,
delay: 0,
})),
[position]
);
const accentPaths: PathData[] = useMemo(
() =>
Array.from({ length: 10 }, (_, i) => ({
id: generateUniqueId("accent"),
d: generateAestheticPath(i, position, "accent"),
opacity: 0.08 + i * 0.12,
width: 2 + i * 0.2,
duration: 15,
delay: 0,
})),
[position]
);
// Shared animation configuration
const sharedAnimationProps = {
opacity: 1,
scale: 1,
};
const sharedTransition = {
opacity: { duration: 1 },
scale: { duration: 1 },
};
return (
Background Paths
{primaryPaths.map((path) => (
))}
{secondaryPaths.map((path) => (
))}
{accentPaths.map((path) => (
))}
);
});
// Memoized AnimatedTitle component
const AnimatedTitle = memo(function AnimatedTitle({
title,
}: {
title: string;
}) {
return (
{title}
);
});
export default memo(function BackgroundPaths({
title = "Background Paths",
}: {
title?: string;
}) {
return (
);
});
```
---
# Beams Background
> Animated background of drifting light beams rendered on an HTML canvas with configurable intensity. Built with React, Tailwind CSS and Motion.
Source: https://kokonutui.com/docs/backgrounds/beams-background
## Installation
```bash
npx shadcn@latest add @kokonutui/beams-background
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Beams Background
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
interface AnimatedGradientBackgroundProps {
className?: string;
children?: React.ReactNode;
intensity?: "subtle" | "medium" | "strong";
}
interface Beam {
x: number;
y: number;
width: number;
length: number;
angle: number;
speed: number;
opacity: number;
hue: number;
pulse: number;
pulseSpeed: number;
}
function createBeam(width: number, height: number, isDarkMode: boolean): Beam {
const angle = -35 + Math.random() * 10;
const hueBase = isDarkMode ? 190 : 210;
const hueRange = isDarkMode ? 70 : 50;
return {
x: Math.random() * width * 1.5 - width * 0.25,
y: Math.random() * height * 1.5 - height * 0.25,
width: 30 + Math.random() * 60,
length: height * 2.5,
angle,
speed: 0.6 + Math.random() * 1.2,
opacity: 0.12 + Math.random() * 0.16,
hue: hueBase + Math.random() * hueRange,
pulse: Math.random() * Math.PI * 2,
pulseSpeed: 0.02 + Math.random() * 0.03,
};
}
export default function BeamsBackground({
className,
intensity = "strong",
}: AnimatedGradientBackgroundProps) {
const canvasRef = useRef(null);
const beamsRef = useRef([]);
const animationFrameRef = useRef(0);
const MINIMUM_BEAMS = 20;
const isDarkModeRef = useRef(false);
const opacityMap = {
subtle: 0.7,
medium: 0.85,
strong: 1,
};
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Check for dark mode
const updateDarkMode = () => {
isDarkModeRef.current =
document.documentElement.classList.contains("dark");
};
const observer = new MutationObserver(updateDarkMode);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
updateDarkMode();
const updateCanvasSize = () => {
const dpr = window.devicePixelRatio || 1;
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
canvas.style.width = `${window.innerWidth}px`;
canvas.style.height = `${window.innerHeight}px`;
ctx.scale(dpr, dpr);
const totalBeams = MINIMUM_BEAMS * 1.5;
beamsRef.current = Array.from({ length: totalBeams }, () =>
createBeam(canvas.width, canvas.height, isDarkModeRef.current)
);
};
updateCanvasSize();
window.addEventListener("resize", updateCanvasSize);
function resetBeam(beam: Beam, index: number, totalBeams: number) {
if (!canvas) return beam;
const column = index % 3;
const spacing = canvas.width / 3;
const hueBase = isDarkModeRef.current ? 190 : 210;
const hueRange = isDarkModeRef.current ? 70 : 50;
beam.y = canvas.height + 100;
beam.x =
column * spacing + spacing / 2 + (Math.random() - 0.5) * spacing * 0.5;
beam.width = 100 + Math.random() * 100;
beam.speed = 0.5 + Math.random() * 0.4;
beam.hue = hueBase + (index * hueRange) / totalBeams;
beam.opacity = 0.2 + Math.random() * 0.1;
return beam;
}
function drawBeam(ctx: CanvasRenderingContext2D, beam: Beam) {
ctx.save();
ctx.translate(beam.x, beam.y);
ctx.rotate((beam.angle * Math.PI) / 180);
const pulsingOpacity =
beam.opacity *
(0.8 + Math.sin(beam.pulse) * 0.2) *
opacityMap[intensity];
const gradient = ctx.createLinearGradient(0, 0, 0, beam.length);
const saturation = isDarkModeRef.current ? "85%" : "75%";
const lightness = isDarkModeRef.current ? "65%" : "45%";
gradient.addColorStop(
0,
`hsla(${beam.hue}, ${saturation}, ${lightness}, 0)`
);
gradient.addColorStop(
0.1,
`hsla(${beam.hue}, ${saturation}, ${lightness}, ${
pulsingOpacity * 0.5
})`
);
gradient.addColorStop(
0.4,
`hsla(${beam.hue}, ${saturation}, ${lightness}, ${pulsingOpacity})`
);
gradient.addColorStop(
0.6,
`hsla(${beam.hue}, ${saturation}, ${lightness}, ${pulsingOpacity})`
);
gradient.addColorStop(
0.9,
`hsla(${beam.hue}, ${saturation}, ${lightness}, ${
pulsingOpacity * 0.5
})`
);
gradient.addColorStop(
1,
`hsla(${beam.hue}, ${saturation}, ${lightness}, 0)`
);
ctx.fillStyle = gradient;
ctx.fillRect(-beam.width / 2, 0, beam.width, beam.length);
ctx.restore();
}
function animate() {
if (!(canvas && ctx)) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.filter = "blur(35px)";
const totalBeams = beamsRef.current.length;
beamsRef.current.forEach((beam, index) => {
beam.y -= beam.speed;
beam.pulse += beam.pulseSpeed;
// Reset beam when it goes off screen
if (beam.y + beam.length < -100) {
resetBeam(beam, index, totalBeams);
}
drawBeam(ctx, beam);
});
animationFrameRef.current = requestAnimationFrame(animate);
}
animate();
return () => {
window.removeEventListener("resize", updateCanvasSize);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
observer.disconnect();
};
}, [intensity]);
return (
);
}
```
---
# Flow Field
> Canvas particle flow field background with noise-driven streams of glowing light, color themes and density options. Built with React and Motion.
Source: https://kokonutui.com/docs/backgrounds/flow-field
## Installation
```bash
npx shadcn@latest add @kokonutui/flow-field
```
## Source
```tsx
"use client";
/**
* @name: FlowField
* @description: Canvas particle flow field background β organic noise-driven streams of glowing light.
* @version: 1.0.0
* @author: @dorian_baffier
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import type { ReactNode } from "react";
import { useEffect, useRef } from "react";
import { cn } from "@/lib/utils";
// βββ Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
type ColorTheme = "aurora" | "ember" | "ocean";
type ParticleDensity = "sparse" | "medium" | "dense";
interface Particle {
x: number;
y: number;
speed: number;
hue: number;
life: number;
maxLife: number;
}
interface ThemeConfig {
hueStart: number;
hueRange: number;
saturation: number;
lightness: number;
bg: string;
trailAlpha: number;
}
export interface FlowFieldProps {
className?: string;
children?: ReactNode;
theme?: ColorTheme;
density?: ParticleDensity;
}
// βββ Constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const PARTICLE_COUNTS: Record = {
sparse: 600,
medium: 1200,
dense: 2000,
} as const;
const THEMES: Record = {
aurora: {
hueStart: 120,
hueRange: 200,
saturation: 90,
lightness: 62,
bg: "5, 5, 8",
trailAlpha: 0.06,
},
ember: {
hueStart: 0,
hueRange: 55,
saturation: 95,
lightness: 58,
bg: "8, 4, 2",
trailAlpha: 0.07,
},
ocean: {
hueStart: 180,
hueRange: 90,
saturation: 88,
lightness: 60,
bg: "2, 6, 10",
trailAlpha: 0.06,
},
} as const;
// βββ Noise / vector-field βββββββββββββββββββββββββββββββββββββββββββββββββββββ
/**
* Smooth organic 2D noise via a multi-octave trigonometric series.
* Returns an angle in radians that evolves continuously with time `t`.
*/
function fieldAngle(x: number, y: number, t: number): number {
const s = 0.0025;
return (
Math.sin(x * s + t * 0.0007) * Math.PI +
Math.cos(y * s + t * 0.0005) * Math.PI +
Math.sin((x + y) * s * 0.6 + t * 0.0009) * Math.PI * 0.6 +
Math.cos((x - y) * s * 0.4 + t * 0.0006) * Math.PI * 0.4
);
}
// βββ Default hero content βββββββββββββββββββββββββββββββββββββββββββββββββββββ
function DefaultContent() {
return (
Chaos finds its
own beauty
Thousands of particles drift through an organic noise field, painting
luminous trails that shift and spiral endlessly.
);
}
// βββ Main component βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export default function FlowField({
className,
children,
theme = "aurora",
density = "medium",
}: FlowFieldProps) {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const cfg = THEMES[theme];
const count = PARTICLE_COUNTS[density];
const dpr = window.devicePixelRatio ?? 1;
let width = 0;
let height = 0;
let animId = 0;
let time = 0;
let particles: Particle[] = [];
const spawnParticle = (): Particle => {
const maxLife = 200 + Math.floor(Math.random() * 300);
return {
x: Math.random() * width,
y: Math.random() * height,
speed: 1.1 + Math.random() * 1.8,
hue: cfg.hueStart + Math.random() * cfg.hueRange,
life: Math.floor(Math.random() * maxLife),
maxLife,
};
};
const resize = () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.scale(dpr, dpr);
// Fill dark base on resize
ctx.fillStyle = `rgb(${cfg.bg})`;
ctx.fillRect(0, 0, width, height);
// Re-seed particles spread across the canvas
particles = Array.from({ length: count }, spawnParticle);
};
const render = () => {
time++;
// Fade previous frame β each dot persists ~16 frames, creating soft trails
ctx.fillStyle = `rgba(${cfg.bg}, ${cfg.trailAlpha})`;
ctx.fillRect(0, 0, width, height);
for (const p of particles) {
const angle = fieldAngle(p.x, p.y, time);
p.x += Math.cos(angle) * p.speed;
p.y += Math.sin(angle) * p.speed;
p.life++;
// Respawn aged-out particles at a random position
if (p.life > p.maxLife) {
p.x = Math.random() * width;
p.y = Math.random() * height;
p.life = 0;
p.hue = cfg.hueStart + Math.random() * cfg.hueRange;
continue;
}
// Wrap edges
if (p.x < 0) p.x += width;
else if (p.x > width) p.x -= width;
if (p.y < 0) p.y += height;
else if (p.y > height) p.y -= height;
// Fade in / out over particle lifetime
const progress = p.life / p.maxLife;
const fadeIn = Math.min(progress * 8, 1);
const fadeOut = Math.min((1 - progress) * 6, 1);
const alpha = fadeIn * fadeOut * 0.9;
// Hue shifts subtly with field direction for color variety
const hueMod = (p.hue + (angle / (Math.PI * 2)) * 70 + 360) % 360;
ctx.beginPath();
ctx.arc(p.x, p.y, 1.3, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${hueMod}, ${cfg.saturation}%, ${cfg.lightness}%, ${alpha})`;
ctx.fill();
}
animId = requestAnimationFrame(render);
};
resize();
window.addEventListener("resize", resize);
render();
return () => {
cancelAnimationFrame(animId);
window.removeEventListener("resize", resize);
};
}, [theme, density]);
const bgColor = THEMES[theme].bg;
return (
{/* Radial vignette β focuses center, dims edges */}
{/* Soft top / bottom fades */}
{children ??
}
);
}
```
---
# Shapes Hero
> Hero section with translucent geometric shapes that drop in and float over a gradient backdrop. Built with React, Tailwind CSS and Motion.
Source: https://kokonutui.com/docs/backgrounds/shape-hero
## Installation
```bash
npx shadcn@latest add @kokonutui/shape-hero
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Shape Hero
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { Pacifico } from "next/font/google";
import { cn } from "@/lib/utils";
const pacifico = Pacifico({
subsets: ["latin"],
weight: ["400"],
variable: "--font-pacifico",
});
function ElegantShape({
className,
delay = 0,
width = 400,
height = 100,
rotate = 0,
gradient = "from-white/[0.08]",
borderRadius = 16,
}: {
className?: string;
delay?: number;
width?: number;
height?: number;
rotate?: number;
gradient?: string;
borderRadius?: number;
}) {
return (
);
}
export default function ShapeHero({
title1 = "Elevate Your",
title2 = "Digital Vision",
}: {
title1?: string;
title2?: string;
}) {
const fadeUpVariants = {
hidden: { opacity: 0, y: 30 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: {
duration: 1,
delay: 0.5 + i * 0.2,
ease: [0.25, 0.4, 0.25, 1],
},
}),
};
return (
{/* Tall rectangle - top left */}
{/* Wide rectangle - bottom right */}
{/* Square - middle left */}
{/* Small rectangle - top right */}
{/* New shapes */}
{/* Medium rectangle - center right */}
{/* Small square - bottom left */}
{/* Tiny rectangle - top center */}
{/* Wide rectangle - middle */}
{title1}
{title2}
UI Components built with Tailwind CSS.
);
}
```
---
# Magnet Button
> Magnetic button that pulls floating particles to its center on hover using Motion spring animations, built with React and Tailwind CSS.
Source: https://kokonutui.com/docs/buttons/attract-button
## Installation
```bash
npx shadcn@latest add @kokonutui/attract-button
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Attract Button
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { Magnet } from "lucide-react";
import { motion, useAnimation } from "motion/react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface AttractButtonProps
extends React.ButtonHTMLAttributes {
particleCount?: number;
attractRadius?: number;
}
interface Particle {
id: number;
x: number;
y: number;
}
export default function AttractButton({
className,
particleCount = 12,
attractRadius = 50,
...props
}: AttractButtonProps) {
const [isAttracting, setIsAttracting] = useState(false);
const [particles, setParticles] = useState([]);
const particlesControl = useAnimation();
useEffect(() => {
const newParticles = Array.from({ length: particleCount }, (_, i) => ({
id: i,
x: Math.random() * 360 - 180,
y: Math.random() * 360 - 180,
}));
setParticles(newParticles);
}, [particleCount]);
const handleInteractionStart = useCallback(async () => {
setIsAttracting(true);
await particlesControl.start({
x: 0,
y: 0,
transition: {
type: "spring",
stiffness: 50,
damping: 10,
},
});
}, [particlesControl]);
const handleInteractionEnd = useCallback(async () => {
setIsAttracting(false);
await particlesControl.start((i) => ({
x: particles[i].x,
y: particles[i].y,
transition: {
type: "spring",
stiffness: 100,
damping: 15,
},
}));
}, [particlesControl, particles]);
return (
{particles.map((_, index) => (
))}
{isAttracting ? "Attracting" : "Hover me"}
);
}
```
---
# Command Button
> Keyboard shortcut button with a Command icon, hover shimmer sweep, and smooth CSS transitions, built with React and Tailwind CSS.
Source: https://kokonutui.com/docs/buttons/command-button
## Installation
```bash
npx shadcn@latest add @kokonutui/command-button
```
## Source
```tsx
import { Command } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
/**
* @author: @dorianbaffier
* @description: Command Button
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
export default function CommandButton({
className,
children,
...props
}: React.ButtonHTMLAttributes & {
children?: React.ReactNode;
}) {
return (
{children || "CMD + K"}
);
}
```
---
# Gradient Button
> Layered gradient button with emerald, purple, and orange variants, soft inner glow, and hover overlay, styled entirely with Tailwind CSS in React.
Source: https://kokonutui.com/docs/buttons/gradient-button
## Installation
```bash
npx shadcn@latest add @kokonutui/gradient-button
```
## Source
```tsx
/**
* @author: @dorianbaffier
* @description: Gradient Button
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type ColorVariant = "emerald" | "purple" | "orange";
interface GradientColors {
dark: {
border: string;
overlay: string;
accent: string;
text: string;
glow: string;
textGlow: string;
hover: string;
};
light: {
border: string;
base: string;
overlay: string;
accent: string;
text: string;
glow: string;
hover: string;
};
}
interface GradientButtonProps
extends React.ButtonHTMLAttributes {
icon?: string;
label?: string;
className?: string;
variant?: ColorVariant;
}
const gradientColors: Record = {
emerald: {
dark: {
border: "from-[#336C4F] via-[#0C1F21] to-[#0D6437]",
overlay: "from-[#347B52]/40 via-[#0C1F21] to-[#0D6437]/30",
accent: "from-[#87F6B7]/10 via-[#0C1F21] to-[#17362A]/50",
text: "from-[#8AEECA] to-[#73F8A8]",
glow: "rgba(135,246,183,0.1)",
textGlow: "rgba(135,246,183,0.4)",
hover: "from-[#17362A]/20 via-[#87F6B7]/10 to-[#17362A]/20",
},
light: {
border: "from-emerald-400 via-emerald-300 to-emerald-200",
base: "from-emerald-50 via-emerald-50/80 to-emerald-50/90",
overlay: "from-emerald-300/30 via-emerald-200/20 to-emerald-400/20",
accent: "from-emerald-400/20 via-emerald-300/10 to-emerald-200/30",
text: "from-emerald-700 to-emerald-600",
glow: "rgba(52,211,153,0.2)",
hover: "from-emerald-300/30 via-emerald-200/20 to-emerald-300/30",
},
},
purple: {
dark: {
border: "from-[#6B46C1] via-[#0C1F21] to-[#553C9A]",
overlay: "from-[#7E22CE]/40 via-[#0C1F21] to-[#6B46C1]/30",
accent: "from-[#E9D8FD]/10 via-[#0C1F21] to-[#44337A]/50",
text: "from-[#E9D8FD] to-[#D6BCFA]",
glow: "rgba(159,122,234,0.1)",
textGlow: "rgba(159,122,234,0.4)",
hover: "from-[#44337A]/20 via-[#B794F4]/10 to-[#44337A]/20",
},
light: {
border: "from-purple-400 via-purple-300 to-purple-200",
base: "from-purple-50 via-purple-50/80 to-purple-50/90",
overlay: "from-purple-300/30 via-purple-200/20 to-purple-400/20",
accent: "from-purple-400/20 via-purple-300/10 to-purple-200/30",
text: "from-purple-700 to-purple-600",
glow: "rgba(159,122,234,0.2)",
hover: "from-purple-300/30 via-purple-200/20 to-purple-300/30",
},
},
orange: {
dark: {
border: "from-[#C05621] via-[#0C1F21] to-[#9C4221]",
overlay: "from-[#DD6B20]/40 via-[#0C1F21] to-[#C05621]/30",
accent: "from-[#FED7AA]/10 via-[#0C1F21] to-[#7B341E]/50",
text: "from-[#FED7AA] to-[#FBD38D]",
glow: "rgba(237,137,54,0.1)",
textGlow: "rgba(237,137,54,0.4)",
hover: "from-[#7B341E]/20 via-[#ED8936]/10 to-[#7B341E]/20",
},
light: {
border: "from-orange-400 via-orange-300 to-orange-200",
base: "from-orange-50 via-orange-50/80 to-orange-50/90",
overlay: "from-orange-300/30 via-orange-200/20 to-orange-400/20",
accent: "from-orange-400/20 via-orange-300/10 to-orange-200/30",
text: "from-orange-700 to-orange-600",
glow: "rgba(237,137,54,0.2)",
hover: "from-orange-300/30 via-orange-200/20 to-orange-300/30",
},
},
};
export default function GradientButton({
label = "Welcome",
className,
variant = "emerald",
...props
}: GradientButtonProps) {
const colors = gradientColors[variant];
return (
{label}
);
}
```
---
# Hold Button
> Press-and-hold confirmation button with an animated progress fill, configurable hold duration, and color variants, built with React and Motion.
Source: https://kokonutui.com/docs/buttons/hold-button
## Installation
```bash
npx shadcn@latest add @kokonutui/hold-button
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Hold Button
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { cva, type VariantProps } from "class-variance-authority";
import {
AlertCircleIcon,
ArchiveXIcon,
BanIcon,
Trash2Icon,
XCircleIcon,
} from "lucide-react";
import { motion, useAnimation } from "motion/react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const holdButtonVariants = cva("relative min-w-40 touch-none overflow-hidden", {
variants: {
variant: {
red: [
"bg-red-100 dark:bg-red-200",
"hover:bg-red-100 dark:hover:bg-red-200",
"text-red-500 dark:text-red-600",
"border border-red-200 dark:border-red-300",
],
green: [
"bg-green-100 dark:bg-green-200",
"hover:bg-green-100 dark:hover:bg-green-200",
"text-green-500 dark:text-green-600",
"border border-green-200 dark:border-green-300",
],
blue: [
"bg-blue-100 dark:bg-blue-200",
"hover:bg-blue-100 dark:hover:bg-blue-200",
"text-blue-500 dark:text-blue-600",
"border border-blue-200 dark:border-blue-300",
],
orange: [
"bg-orange-100 dark:bg-orange-200",
"hover:bg-orange-100 dark:hover:bg-orange-200",
"text-orange-500 dark:text-orange-600",
"border border-orange-200 dark:border-orange-300",
],
grey: [
"bg-gray-100 dark:bg-gray-200",
"hover:bg-gray-100 dark:hover:bg-gray-200",
"text-gray-500 dark:text-gray-600",
"border border-gray-200 dark:border-gray-300",
],
},
},
defaultVariants: {
variant: "red",
},
});
interface HoldButtonProps
extends React.ButtonHTMLAttributes,
VariantProps {
holdDuration?: number;
}
export default function HoldButton({
className,
variant = "red",
holdDuration = 3000,
...props
}: HoldButtonProps) {
const [isHolding, setIsHolding] = useState(false);
const controls = useAnimation();
async function handleHoldStart() {
setIsHolding(true);
controls.set({ width: "0%" });
await controls.start({
width: "100%",
transition: {
duration: holdDuration / 1000,
ease: "linear",
},
});
}
function handleHoldEnd() {
setIsHolding(false);
controls.stop();
controls.start({
width: "0%",
transition: { duration: 0.1 },
});
}
return (
{(variant === "red" || !variant) && }
{variant === "green" && }
{variant === "blue" && }
{variant === "orange" && }
{variant === "grey" && }
{isHolding ? "Release" : "Hold me"}
);
}
```
---
# Particle Button
> Button that bursts animated particles from its center on click with a quick scale press effect, built with React, Motion, and Tailwind CSS.
Source: https://kokonutui.com/docs/buttons/particle-button
## Installation
```bash
npx shadcn@latest add @kokonutui/particle-button
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Particle Button
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { MousePointerClick } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { type RefObject, useRef, useState } from "react";
import type { ButtonProps } from "@/components/ui/button";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface ParticleButtonProps extends ButtonProps {
onSuccess?: () => void;
successDuration?: number;
}
function SuccessParticles({
buttonRef,
}: {
buttonRef: React.RefObject;
}) {
const rect = buttonRef.current?.getBoundingClientRect();
if (!rect) return null;
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
return (
{[...Array(6)].map((_, i) => (
))}
);
}
export default function ParticleButton({
children,
onClick,
onSuccess,
successDuration = 1000,
className,
...props
}: ParticleButtonProps) {
const [showParticles, setShowParticles] = useState(false);
const buttonRef = useRef(null);
const handleClick = async (e: React.MouseEvent) => {
setShowParticles(true);
setTimeout(() => {
setShowParticles(false);
}, successDuration);
};
return (
<>
{showParticles && (
}
/>
)}
{children}
>
);
}
```
---
# Slide Text Button
> Animated link button with a vertical text slide on hover and default and ghost variants, built with React, Motion, and Tailwind CSS.
Source: https://kokonutui.com/docs/buttons/slide-text-button
## Installation
```bash
npx shadcn@latest add @kokonutui/slide-text-button
```
## Source
```tsx
"use client";
/**
* @author: @kokonut-labs
* @description: Slide Text Button with animated vertical text transition
* @version: 1.0.0
* @date: 2025-11-02
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import Link from "next/link";
import { cn } from "@/lib/utils";
interface SlideTextButtonProps
extends React.AnchorHTMLAttributes {
text?: string;
hoverText?: string;
href?: string;
className?: string;
variant?: "default" | "ghost";
}
export default function SlideTextButton({
text = "Browse Components",
hoverText,
href = "/docs",
className,
variant = "default",
...props
}: SlideTextButtonProps) {
const slideText = hoverText ?? text;
const variantStyles =
variant === "ghost"
? "border border-black/10 text-black hover:bg-black/5 dark:border-white/10 dark:text-white dark:hover:bg-white/5"
: "bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90";
return (
{text}
{slideText}
);
}
```
---
# Social Button
> Share button that expands into a row of social icon buttons with staggered Motion animations on hover, built with React and Tailwind CSS.
Source: https://kokonutui.com/docs/buttons/social-button
## Installation
```bash
npx shadcn@latest add @kokonutui/social-button
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Social Button
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import type { LucideIcon } from "lucide-react";
import { Instagram, Link, Linkedin, Twitter } from "lucide-react";
import { motion } from "motion/react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface ShareItem {
icon: LucideIcon;
label: string;
}
interface SocialButtonProps
extends React.ButtonHTMLAttributes {
label?: string;
items?: ShareItem[];
onShare?: (index: number, item: ShareItem) => void;
className?: string;
}
const DEFAULT_SHARE_ITEMS: ShareItem[] = [
{ icon: Twitter, label: "Share on Twitter" },
{ icon: Instagram, label: "Share on Instagram" },
{ icon: Linkedin, label: "Share on LinkedIn" },
{ icon: Link, label: "Copy link" },
];
export default function SocialButton({
label = "Share",
items = DEFAULT_SHARE_ITEMS,
onShare,
className,
...props
}: SocialButtonProps) {
const [isVisible, setIsVisible] = useState(false);
const [activeIndex, setActiveIndex] = useState(null);
const handleShare = (index: number) => {
setActiveIndex(index);
onShare?.(index, items[index]);
setTimeout(() => setActiveIndex(null), 300);
};
return (
setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{label}
{items.map((button, i) => (
handleShare(i)}
transition={{
duration: 0.3,
ease: [0.23, 1, 0.32, 1],
delay: isVisible ? i * 0.05 : 0,
}}
type="button"
>
))}
);
}
```
---
# Switch Button
> Light and dark theme toggle button with a rotating sun icon and shimmer hover effect, built with React, next-themes, and Tailwind CSS.
Source: https://kokonutui.com/docs/buttons/switch-button
## Installation
```bash
npx shadcn@latest add @kokonutui/switch-button
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Switch Button
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
interface SwitchButtonProps
extends React.ButtonHTMLAttributes {
variant?: "minimal";
size?: "sm" | "default" | "lg";
showLabel?: boolean;
}
export default function SwitchButton({
className,
variant = "minimal",
size = "default",
showLabel = true,
...props
}: SwitchButtonProps) {
const { setTheme, theme } = useTheme();
const handleThemeToggle = () => {
setTheme(theme === "dark" ? "light" : "dark");
};
const variants = {
minimal: [
"rounded-lg",
"bg-gradient-to-b from-zinc-50/95 to-zinc-100/95 dark:from-zinc-800/95 dark:to-zinc-900/95",
"hover:from-zinc-100/95 hover:to-zinc-200/95 dark:hover:from-zinc-700/95 dark:hover:to-zinc-800/95",
"border border-zinc-200 dark:border-zinc-700/80",
"hover:border-zinc-300 dark:hover:border-zinc-600",
"shadow-[0_1px_2px_-1px_rgb(0_0_0/0.1),0_1px_3px_-2px_rgb(0_0_0/0.1)] dark:shadow-[0_1px_2px_-1px_rgb(0_0_0/0.3),0_1px_3px_-2px_rgb(0_0_0/0.3)]",
"hover:shadow-[0_2px_4px_-2px_rgb(0_0_0/0.15),0_2px_6px_-3px_rgb(0_0_0/0.15)] dark:hover:shadow-[0_2px_4px_-2px_rgb(0_0_0/0.4),0_2px_6px_-3px_rgb(0_0_0/0.4)]",
"active:shadow-[0_0px_1px_0_rgb(0_0_0/0.1)] dark:active:shadow-[0_0px_1px_0_rgb(0_0_0/0.2)]",
"transition-all duration-200 ease-out",
"backdrop-blur-sm",
"relative",
"after:absolute after:inset-0 after:rounded-lg after:bg-gradient-to-t after:from-white/10 after:to-transparent after:opacity-0 hover:after:opacity-100 after:transition-opacity",
"before:absolute before:inset-[1px] before:rounded-[7px] before:bg-gradient-to-b before:from-white/20 before:to-transparent before:opacity-0 hover:before:opacity-100 before:transition-opacity dark:before:from-white/5",
],
};
const sizes = {
sm: "h-8 px-3 text-sm",
default: "h-10 px-4",
lg: "h-11 px-5",
};
return (
{showLabel && (
Light
Dark
Light
)}
);
}
```
---
# V0 Button
> Open in v0 button that sends a component registry JSON to v0.dev for instant editing, built with React, the shadcn Button, and Tailwind CSS.
Source: https://kokonutui.com/docs/buttons/v0-button
## Installation
```bash
npx shadcn@latest add @kokonutui/v0-button
```
## Source
```tsx
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
const prePath = process.env.VERCEL_PROJECT_PRODUCTION_URL
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: `https://${process.env.NEXT_PUBLIC_SITE_URL}`;
export default function V0Button({
name = "liquid-glass-card",
className,
}: { name?: string } & React.ComponentProps) {
return (
Open in{" "}
);
}
```
---
# Avatar Picker
> Animated avatar picker with SVG avatar options, colored selection ring and username input. Built with React, Tailwind CSS and Motion.
Source: https://kokonutui.com/docs/inputs/avatar-picker
## Installation
```bash
npx shadcn@latest add @kokonutui/avatar-picker
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Avatar Picker
* @version: 2.0.0
* @date: 2026-02-22
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { Check, ChevronRight, User2 } from "lucide-react";
import type { Variants } from "motion/react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type { ReactNode } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
interface Avatar {
id: number;
svg: ReactNode;
alt: string;
}
// RGB values for the per-avatar color ring on the stage
const AVATAR_RGB: Record = {
1: "255, 0, 91",
2: "255, 125, 16",
3: "255, 0, 91",
4: "137, 252, 179",
};
const avatars: Avatar[] = [
{
id: 1,
svg: (
Avatar 1
),
alt: "Avatar 1",
},
{
id: 2,
svg: (
Avatar 2
),
alt: "Avatar 2",
},
{
id: 3,
svg: (
Avatar 3
),
alt: "Avatar 3",
},
{
id: 4,
svg: (
Avatar 4
),
alt: "Avatar 4",
},
];
interface ProfileSetupProps {
onComplete?: (data: { username: string; avatarId: number }) => void;
className?: string;
}
const containerVariants: Variants = {
initial: { opacity: 0 },
animate: {
opacity: 1,
transition: { staggerChildren: 0.06, delayChildren: 0.05 },
},
};
const thumbnailVariants: Variants = {
initial: { opacity: 0, y: 6 },
animate: {
opacity: 1,
y: 0,
transition: { duration: 0.28, ease: "easeOut" },
},
};
export default function ProfileSetup({
onComplete,
className,
}: ProfileSetupProps) {
const [selectedAvatar, setSelectedAvatar] = useState(avatars[0]);
const [username, setUsername] = useState("");
const [isFocused, setIsFocused] = useState(false);
const shouldReduceMotion = useReducedMotion();
const handleAvatarSelect = (avatar: Avatar) => {
if (avatar.id === selectedAvatar.id) return;
setSelectedAvatar(avatar);
};
const handleSubmit = () => {
if (username.trim() && onComplete) {
onComplete({
username: username.trim(),
avatarId: selectedAvatar.id,
});
}
};
const isValid = username.trim().length >= 3;
const showError = username.trim().length > 0 && username.trim().length < 3;
const rgb = AVATAR_RGB[selectedAvatar.id];
return (
{/* Header */}
Pick Your Avatar
Choose one to get started
{/* Avatar Stage */}
{/*
* Two-div approach: outer div holds the animated color ring
* (no overflow-hidden so box-shadow renders cleanly),
* inner div clips the avatar SVG.
* scale-[4] fills the 160px circle with the avatar's background.
*/}
{/* Animated per-avatar color ring */}
{/* Avatar circle β clips content */}
{/* scale-[4]: 40px SVG Γ 4 = 160px, fills the circle */}
{selectedAvatar.svg}
{/* Avatar name β fades with selection */}
{selectedAvatar.alt}
{/* Thumbnail strip */}
{avatars.map((avatar) => {
const isSelected = selectedAvatar.id === avatar.id;
return (
handleAvatarSelect(avatar)}
type="button"
variants={thumbnailVariants}
whileHover={shouldReduceMotion ? {} : { scale: 1.06 }}
whileTap={shouldReduceMotion ? {} : { scale: 0.94 }}
>
{isSelected && (
)}
);
})}
{/* Username field */}
);
}
```
---
# File Upload
> Drag and drop file upload with animated progress, file validation and error states. Built with React, Tailwind CSS and Motion animations.
Source: https://kokonutui.com/docs/inputs/file-upload
## Installation
```bash
npx shadcn@latest add @kokonutui/file-upload
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: File Upload
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { UploadCloud } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import {
type DragEvent,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { cn } from "@/lib/utils";
type FileStatus = "idle" | "dragging" | "uploading" | "error";
interface FileError {
message: string;
code: string;
}
interface FileUploadProps {
onUploadSuccess?: (file: File) => void;
onUploadError?: (error: FileError) => void;
acceptedFileTypes?: string[];
maxFileSize?: number;
currentFile?: File | null;
onFileRemove?: () => void;
/** Duration in milliseconds for the upload simulation. Defaults to 2000ms (2s), 0 for no simulation */
uploadDelay?: number;
validateFile?: (file: File) => FileError | null;
className?: string;
}
const DEFAULT_MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const UPLOAD_STEP_SIZE = 5;
const FILE_SIZES = [
"Bytes",
"KB",
"MB",
"GB",
"TB",
"PB",
"EB",
"ZB",
"YB",
] as const;
const formatBytes = (bytes: number, decimals = 2): string => {
if (!+bytes) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const i = Math.floor(Math.log(bytes) / Math.log(k));
const unit = FILE_SIZES[i] || FILE_SIZES[FILE_SIZES.length - 1];
return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${unit}`;
};
const UploadIllustration = () => (
);
const UploadingAnimation = ({ progress }: { progress: number }) => (
Upload Progress Indicator
);
export default function FileUpload({
onUploadSuccess = () => {},
onUploadError = () => {},
acceptedFileTypes = [],
maxFileSize = DEFAULT_MAX_FILE_SIZE,
currentFile: initialFile = null,
onFileRemove = () => {},
uploadDelay = 2000,
validateFile = () => null,
className,
}: FileUploadProps) {
const [file, setFile] = useState(initialFile);
const [status, setStatus] = useState("idle");
const [progress, setProgress] = useState(0);
const [error, setError] = useState(null);
const fileInputRef = useRef(null);
const uploadIntervalRef = useRef(null);
useEffect(
() => () => {
if (uploadIntervalRef.current) {
clearInterval(uploadIntervalRef.current);
}
},
[]
);
const validateFileSize = useCallback(
(file: File): FileError | null => {
if (file.size > maxFileSize) {
return {
message: `File size exceeds ${formatBytes(maxFileSize)}`,
code: "FILE_TOO_LARGE",
};
}
return null;
},
[maxFileSize]
);
const validateFileType = useCallback(
(file: File): FileError | null => {
if (!acceptedFileTypes?.length) return null;
const fileType = file.type.toLowerCase();
if (
!acceptedFileTypes.some((type) => fileType.match(type.toLowerCase()))
) {
return {
message: `File type must be ${acceptedFileTypes.join(", ")}`,
code: "INVALID_FILE_TYPE",
};
}
return null;
},
[acceptedFileTypes]
);
const handleError = useCallback(
(error: FileError) => {
setError(error);
setStatus("error");
onUploadError?.(error);
setTimeout(() => {
setError(null);
setStatus("idle");
}, 3000);
},
[onUploadError]
);
const simulateUpload = useCallback(
(uploadingFile: File) => {
let currentProgress = 0;
if (uploadIntervalRef.current) {
clearInterval(uploadIntervalRef.current);
}
uploadIntervalRef.current = setInterval(
() => {
currentProgress += UPLOAD_STEP_SIZE;
if (currentProgress >= 100) {
if (uploadIntervalRef.current) {
clearInterval(uploadIntervalRef.current);
}
setProgress(0);
setStatus("idle");
setFile(null);
onUploadSuccess?.(uploadingFile);
} else {
setStatus((prevStatus) => {
if (prevStatus === "uploading") {
setProgress(currentProgress);
return "uploading";
}
if (uploadIntervalRef.current) {
clearInterval(uploadIntervalRef.current);
}
return prevStatus;
});
}
},
uploadDelay / (100 / UPLOAD_STEP_SIZE)
);
},
[onUploadSuccess, uploadDelay]
);
const handleFileSelect = useCallback(
(selectedFile: File | null) => {
if (!selectedFile) return;
// Reset error state
setError(null);
// Validate file
const sizeError = validateFileSize(selectedFile);
if (sizeError) {
handleError(sizeError);
return;
}
const typeError = validateFileType(selectedFile);
if (typeError) {
handleError(typeError);
return;
}
const customError = validateFile?.(selectedFile);
if (customError) {
handleError(customError);
return;
}
setFile(selectedFile);
setStatus("uploading");
setProgress(0);
simulateUpload(selectedFile);
},
[
simulateUpload,
validateFileSize,
validateFileType,
validateFile,
handleError,
]
);
const handleDragOver = useCallback((e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setStatus((prev) => (prev !== "uploading" ? "dragging" : prev));
}, []);
const handleDragLeave = useCallback((e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setStatus((prev) => (prev === "dragging" ? "idle" : prev));
}, []);
const handleDrop = useCallback(
(e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (status === "uploading") return;
setStatus("idle");
const droppedFile = e.dataTransfer.files?.[0];
if (droppedFile) handleFileSelect(droppedFile);
},
[status, handleFileSelect]
);
const handleFileInputChange = useCallback(
(e: React.ChangeEvent) => {
const selectedFile = e.target.files?.[0];
handleFileSelect(selectedFile || null);
if (e.target) e.target.value = "";
},
[handleFileSelect]
);
const triggerFileInput = useCallback(() => {
if (status === "uploading") return;
fileInputRef.current?.click();
}, [status]);
const resetState = useCallback(() => {
setFile(null);
setStatus("idle");
setProgress(0);
if (onFileRemove) onFileRemove();
}, [onFileRemove]);
return (
{status === "idle" || status === "dragging" ? (
Drag and drop or
{acceptedFileTypes?.length
? `${acceptedFileTypes
.map((t) => t.split("/")[1])
.join(", ")
.toUpperCase()}`
: "SVG, PNG, JPG or GIF"}{" "}
{maxFileSize && `up to ${formatBytes(maxFileSize)}`}
Upload File
or drag and drop your file here
) : status === "uploading" ? (
{file?.name}
{formatBytes(file?.size || 0)}
{Math.round(progress)}%
Cancel
) : null}
{error && (
{error.message}
)}
);
}
FileUpload.displayName = "FileUpload";
```
---
# Loader
> Animated loading indicator with rotating gradient rings, size variants and customizable title text. Built with React, Tailwind CSS and Motion.
Source: https://kokonutui.com/docs/inputs/loader
## Installation
```bash
npx shadcn@latest add @kokonutui/loader
```
## Source
```tsx
"use client";
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface LoaderProps extends React.HTMLAttributes {
title?: string;
subtitle?: string;
size?: "sm" | "md" | "lg";
}
export default function Loader({
title = "Configuring your account...",
subtitle = "Please wait while we prepare everything for you",
size = "md",
className,
...props
}: LoaderProps) {
const sizeConfig = {
sm: {
container: "size-20",
titleClass: "text-sm/tight font-medium",
subtitleClass: "text-xs/relaxed",
spacing: "space-y-2",
maxWidth: "max-w-48",
},
md: {
container: "size-32",
titleClass: "text-base/snug font-medium",
subtitleClass: "text-sm/relaxed",
spacing: "space-y-3",
maxWidth: "max-w-56",
},
lg: {
container: "size-40",
titleClass: "text-lg/tight font-semibold",
subtitleClass: "text-base/relaxed",
spacing: "space-y-4",
maxWidth: "max-w-64",
},
};
const config = sizeConfig[size];
return (
{/* Enhanced Monochrome Loader */}
{/* Outer elegant ring with shimmer */}
{/* Primary animated ring with gradient */}
{/* Secondary elegant ring - counter rotation */}
{/* Accent particles */}
{/* Dark mode variants */}
{/* Enhanced Typography with Breathing Animation */}
{/* Clean title with subtle animation */}
{title}
{/* Clean subtitle with subtle animation */}
{subtitle}
);
}
```
---
# Team Selector
> Animated team size selector with stacked overlapping avatars, spring transitions and increment controls. Built with React, Tailwind CSS and Motion.
Source: https://kokonutui.com/docs/inputs/team-selector
## Installation
```bash
npx shadcn@latest add @kokonutui/team-selector
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Team Selector
* @version: 3.0.0
* @date: 2026-04-23
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { Minus, Plus } from "lucide-react";
import {
AnimatePresence,
motion,
useReducedMotion,
type Variants,
} from "motion/react";
import Image from "next/image";
import { useRef, useState } from "react";
const AVATAR_OVERLAP = 12;
const DICEBEAR_STYLE = "notionists-neutral";
const dicebearUrl = (seed: string) =>
`https://api.dicebear.com/9.x/${DICEBEAR_STYLE}/svg?seed=${encodeURIComponent(seed)}&backgroundColor=f4f4f5,e4e4e7,d4d4d8,a1a1aa`;
interface TeamMember {
id: string;
name: string;
avatarUrl: string;
}
const DEFAULT_MEMBERS: TeamMember[] = [
{ id: "member-1", name: "Alex Rivera", avatarUrl: dicebearUrl("Alex") },
{ id: "member-2", name: "Blair Kim", avatarUrl: dicebearUrl("Blair") },
{ id: "member-3", name: "Casey Lin", avatarUrl: dicebearUrl("Casey") },
{ id: "member-4", name: "Devon Marsh", avatarUrl: dicebearUrl("Devon") },
];
const animations = {
avatar: {
visible: {
opacity: 1,
scale: 1,
transition: {
type: "spring",
stiffness: 260,
damping: 22,
mass: 0.6,
},
},
hidden: {
opacity: 0,
scale: 0.85,
transition: { duration: 0.18, ease: "easeOut" },
},
} satisfies Variants,
vibration: {
idle: { x: 0 },
shake: {
x: [-3, 3, -2, 2, 0] as const,
transition: { duration: 0.28, ease: "easeOut" },
},
} satisfies Variants,
} as const;
interface TeamSelectorProps {
members?: TeamMember[];
defaultValue?: number;
onChange?: (size: number) => void;
label?: string;
className?: string;
}
export default function TeamSelector({
members = DEFAULT_MEMBERS,
defaultValue = 1,
onChange,
label = "Team Size",
className = "",
}: TeamSelectorProps) {
const maxTeamSize = members.length;
const [peopleCount, setPeopleCount] = useState(defaultValue);
const [isVibrating, setIsVibrating] = useState(false);
const directionRef = useRef<1 | -1>(1);
const prefersReducedMotion = useReducedMotion();
const triggerVibration = () => {
if (prefersReducedMotion) {
return;
}
setIsVibrating(true);
setTimeout(() => setIsVibrating(false), 280);
};
const handleIncrement = (e: React.MouseEvent | React.KeyboardEvent) => {
e.preventDefault();
if (peopleCount < maxTeamSize) {
directionRef.current = 1;
const newCount = peopleCount + 1;
setPeopleCount(newCount);
onChange?.(newCount);
} else {
triggerVibration();
}
};
const handleDecrement = (e: React.MouseEvent | React.KeyboardEvent) => {
e.preventDefault();
if (peopleCount > 1) {
directionRef.current = -1;
const newCount = peopleCount - 1;
setPeopleCount(newCount);
onChange?.(newCount);
} else {
triggerVibration();
}
};
const handleKeyDown = (
e: React.KeyboardEvent,
action: "increment" | "decrement"
) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (action === "increment") {
handleIncrement(e);
} else {
handleDecrement(e);
}
}
};
const counterDistance = prefersReducedMotion ? 0 : 10;
return (
{label}
{members.map((member, index) => (
))}
handleKeyDown(e, "decrement")}
type="button"
>
{peopleCount === 1 ? "member" : "members"}
= maxTeamSize}
onClick={handleIncrement}
onKeyDown={(e) => handleKeyDown(e, "increment")}
type="button"
>
);
}
```
---
# Apple Activity Card
> Animated activity rings card inspired by the Apple Fitness app, with progress circles built in React using Tailwind CSS and Motion animations.
Source: https://kokonutui.com/docs/cards/apple-activity-card
## Installation
```bash
npx shadcn@latest add @kokonutui/apple-activity-card
```
## Source
```tsx
"use client";
/**
* @author: @kokonutui
* @description: Apple Activity Card
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface ActivityData {
label: string;
value: number;
color: string;
size: number;
current: number;
target: number;
unit: string;
}
interface CircleProgressProps {
data: ActivityData;
index: number;
}
const activities: ActivityData[] = [
{
label: "MOVE",
value: 85,
color: "#FF2D55",
size: 200,
current: 479,
target: 800,
unit: "CAL",
},
{
label: "EXERCISE",
value: 60,
color: "#A3F900",
size: 160,
current: 24,
target: 30,
unit: "MIN",
},
{
label: "STAND",
value: 30,
color: "#04C7DD",
size: 120,
current: 6,
target: 12,
unit: "HR",
},
];
const CircleProgress = ({ data, index }: CircleProgressProps) => {
const strokeWidth = 16;
const radius = (data.size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const progress = ((100 - data.value) / 100) * circumference;
const gradientId = `gradient-${data.label.toLowerCase()}`;
const gradientUrl = `url(#${gradientId})`;
return (
{`${data.label} Activity Progress - ${data.value}%`}
);
};
const DetailedActivityInfo = () => {
return (
{activities.map((activity) => (
{activity.label}
{activity.current}/{activity.target}
{activity.unit}
))}
);
};
export default function AppleActivityCard({
title = "Activity Rings",
className,
}: {
title?: string;
className?: string;
}) {
return (
{title}
{activities.map((activity, index) => (
))}
);
}
```
---
# Bento Grid
> Responsive bento grid of feature cards with animated charts, counters and timelines, built with React, Tailwind CSS and Motion animations.
Source: https://kokonutui.com/docs/cards/bento-grid
## Installation
```bash
npx shadcn@latest add @kokonutui/bento-grid
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Bento Grid
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import {
ArrowUpRight,
CheckCircle2,
Clock,
Mic,
Plus,
Sparkles,
Zap,
} from "lucide-react";
import {
motion,
useMotionValue,
useTransform,
type Variants,
} from "motion/react";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import Anthropic from "@/components/icons/anthropic";
import AnthropicDark from "@/components/icons/anthropic-dark";
import DeepSeek from "@/components/icons/deepseek";
import Google from "@/components/icons/gemini";
import MistralAI from "@/components/icons/mistral";
import OpenAI from "@/components/icons/open-ai";
import OpenAIDark from "@/components/icons/open-ai-dark";
import { cn } from "@/lib/utils";
interface BentoItem {
id: string;
title: string;
description: string;
icons?: boolean;
href?: string;
feature?:
| "chart"
| "counter"
| "code"
| "timeline"
| "spotlight"
| "icons"
| "typing"
| "metrics";
spotlightItems?: string[];
timeline?: Array<{ year: string; event: string }>;
code?: string;
codeLang?: string;
typingText?: string;
metrics?: Array<{
label: string;
value: number;
suffix?: string;
color?: string;
}>;
statistic?: {
value: string;
label: string;
start?: number;
end?: number;
suffix?: string;
};
size?: "sm" | "md" | "lg";
className?: string;
}
const bentoItems: BentoItem[] = [
{
id: "main",
title: "Building tomorrow's technology",
description:
"We architect and develop enterprise-grade applications that scale seamlessly with cloud-native technologies and microservices.",
href: "#",
feature: "spotlight",
spotlightItems: [
"Microservices architecture",
"Serverless computing",
"Container orchestration",
"API-first design",
"Event-driven systems",
],
size: "lg",
className: "col-span-2 row-span-1 md:col-span-2 md:row-span-1",
},
{
id: "stat1",
title: "AI Agents & Automation",
description:
"Intelligent agents that learn, adapt, and automate complex workflows",
href: "#",
feature: "typing",
typingText:
"const createAgent = async () => {\n const agent = new AIAgent({\n model: 'gpt-4-turbo',\n tools: [codeAnalysis, dataProcessing],\n memory: new ConversationalMemory()\n });\n\n // Train on domain knowledge\n await agent.learn(domainData);\n\n return agent;\n};",
size: "md",
className: "col-span-2 row-span-1 col-start-1 col-end-3",
},
{
id: "partners",
title: "Trusted partners",
description:
"Working with the leading AI and cloud providers to deliver cutting-edge solutions",
icons: true,
href: "#",
feature: "icons",
size: "md",
className: "col-span-1 row-span-1",
},
{
id: "innovation",
title: "Innovation timeline",
description:
"Pioneering the future of AI and cloud computing with breakthrough innovations",
href: "#",
feature: "timeline",
timeline: [
{ year: "2020", event: "Launch of Cloud-Native Platform" },
{ year: "2021", event: "Advanced AI Integration & LLM APIs" },
{ year: "2022", event: "Multi-Agent Systems & RAG Architecture" },
{ year: "2023", event: "Autonomous AI Agents & Neural Networks" },
{
year: "2024",
event: "AGI-Ready Infrastructure & Edge Computing",
},
],
size: "sm",
className: "col-span-1 row-span-1",
},
];
const fadeInUp: Variants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.5,
ease: "easeOut",
},
},
};
const staggerContainer: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.3,
},
},
};
const SpotlightFeature = ({ items }: { items: string[] }) => (
{items.map((item, index) => (
{item}
))}
);
const CounterAnimation = ({
start,
end,
suffix = "",
}: {
start: number;
end: number;
suffix?: string;
}) => {
const [count, setCount] = useState(start);
useEffect(() => {
const duration = 2000;
const frameRate = 1000 / 60;
const totalFrames = Math.round(duration / frameRate);
let currentFrame = 0;
const counter = setInterval(() => {
currentFrame++;
const progress = currentFrame / totalFrames;
const easedProgress = 1 - (1 - progress) ** 3;
const current = start + (end - start) * easedProgress;
setCount(Math.min(current, end));
if (currentFrame === totalFrames) {
clearInterval(counter);
}
}, frameRate);
return () => clearInterval(counter);
}, [start, end]);
return (
{count.toFixed(1).replace(/\.0$/, "")}
{suffix}
);
};
const ChartAnimation = ({ value }: { value: number }) => (
);
const IconsFeature = () => (
OpenAI
Anthropic
Google
Mistral
DeepSeek
More
);
const TimelineFeature = ({
timeline,
}: {
timeline: Array<{ year: string; event: string }>;
}) => (
{timeline.map((item) => (
))}
);
const TypingCodeFeature = ({ text }: { text: string }) => {
const [displayedText, setDisplayedText] = useState("");
const [currentIndex, setCurrentIndex] = useState(0);
const terminalRef = useRef(null);
useEffect(() => {
if (currentIndex < text.length) {
const timeout = setTimeout(
() => {
setDisplayedText((prev) => prev + text[currentIndex]);
setCurrentIndex((prev) => prev + 1);
if (terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
},
Math.random() * 30 + 10
); // Random typing speed for realistic effect
return () => clearTimeout(timeout);
}
}, [currentIndex, text]);
// Reset animation when component unmounts and remounts
useEffect(() => {
setDisplayedText("");
setCurrentIndex(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
);
};
const MetricsFeature = ({
metrics,
}: {
metrics: Array<{
label: string;
value: number;
suffix?: string;
color?: string;
}>;
}) => {
const getColorClass = (color = "emerald") => {
const colors = {
emerald: "bg-emerald-500 dark:bg-emerald-400",
blue: "bg-blue-500 dark:bg-blue-400",
violet: "bg-violet-500 dark:bg-violet-400",
amber: "bg-amber-500 dark:bg-amber-400",
rose: "bg-rose-500 dark:bg-rose-400",
};
return colors[color as keyof typeof colors] || colors.emerald;
};
return (
{metrics.map((metric, index) => (
{metric.label === "Uptime" && }
{metric.label === "Response time" && (
)}
{metric.label === "Cost reduction" && (
)}
{metric.label}
{metric.value}
{metric.suffix}
))}
);
};
function AIInput_Voice() {
const [submitted, setSubmitted] = useState(false);
const [time, setTime] = useState(0);
const [isClient, setIsClient] = useState(false);
const [isDemo, setIsDemo] = useState(true);
useEffect(() => {
setIsClient(true);
}, []);
useEffect(() => {
let intervalId: NodeJS.Timeout;
if (submitted) {
intervalId = setInterval(() => {
setTime((t) => t + 1);
}, 1000);
} else {
setTime(0);
}
return () => clearInterval(intervalId);
}, [submitted]);
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, "0")}:${secs
.toString()
.padStart(2, "0")}`;
};
useEffect(() => {
if (!isDemo) return;
let timeoutId: NodeJS.Timeout;
const runAnimation = () => {
setSubmitted(true);
timeoutId = setTimeout(() => {
setSubmitted(false);
timeoutId = setTimeout(runAnimation, 1000);
}, 3000);
};
const initialTimeout = setTimeout(runAnimation, 100);
return () => {
clearTimeout(timeoutId);
clearTimeout(initialTimeout);
};
}, [isDemo]);
const handleClick = () => {
if (isDemo) {
setIsDemo(false);
setSubmitted(false);
} else {
setSubmitted((prev) => !prev);
}
};
return (
{submitted ? (
) : (
)}
{formatTime(time)}
{[...Array(48)].map((_, i) => (
))}
{submitted ? "Listening..." : "Click to speak"}
);
}
const BentoCard = ({ item }: { item: BentoItem }) => {
const [isHovered, setIsHovered] = useState(false);
const x = useMotionValue(0);
const y = useMotionValue(0);
const rotateX = useTransform(y, [-100, 100], [2, -2]);
const rotateY = useTransform(x, [-100, 100], [-2, 2]);
function handleMouseMove(event: React.MouseEvent) {
const rect = event.currentTarget.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
const xPct = mouseX / width - 0.5;
const yPct = mouseY / height - 0.5;
x.set(xPct * 100);
y.set(yPct * 100);
}
function handleMouseLeave() {
x.set(0);
y.set(0);
setIsHovered(false);
}
return (
setIsHovered(true)}
onMouseMove={handleMouseMove}
style={{
rotateX,
rotateY,
transformStyle: "preserve-3d",
}}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
variants={fadeInUp}
whileHover={{ y: -5 }}
>
{item.description}
{/* Feature specific content */}
{item.feature === "spotlight" && item.spotlightItems && (
)}
{item.feature === "counter" && item.statistic && (
)}
{item.feature === "chart" && item.statistic && (
{item.statistic.label}
{item.statistic.end}
{item.statistic.suffix}
)}
{item.feature === "timeline" && item.timeline && (
)}
{item.feature === "icons" &&
}
{item.feature === "typing" && item.typingText && (
)}
{item.feature === "metrics" && item.metrics && (
)}
{item.icons && !item.feature && (
)}
);
};
export default function BentoGrid() {
return (
{/* Bento Grid */}
Voice Assistant
Interact with our AI using natural voice commands. Experience
seamless voice-driven interactions with advanced speech
recognition.
);
}
```
---
# Card Flip
> Interactive card that flips in 3D on hover to reveal features and a call to action, built with React and Tailwind CSS transforms and transitions.
Source: https://kokonutui.com/docs/cards/card-flip
## Installation
```bash
npx shadcn@latest add @kokonutui/card-flip
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Card Flip
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { ArrowRight, Repeat2 } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
export interface CardFlipProps {
title?: string;
subtitle?: string;
description?: string;
features?: string[];
}
export default function CardFlip({
title = "Design Systems",
subtitle = "Explore the fundamentals",
description = "Dive deep into the world of modern UI/UX design.",
features = ["UI/UX", "Modern Design", "Tailwind CSS", "Kokonut UI"],
}: CardFlipProps) {
const [isFlipped, setIsFlipped] = useState(false);
return (
setIsFlipped(true)}
onMouseLeave={() => setIsFlipped(false)}
>
{[...Array(10)].map((_, i) => (
))}
{/* Back of card */}
{features.map((feature, index) => (
))}
);
}
```
---
# Stack
> Stack of product cards that expands on click to reveal details and specs, animated with Motion and styled with React and Tailwind CSS.
Source: https://kokonutui.com/docs/cards/card-stack
## Installation
```bash
npx shadcn@latest add @kokonutui/card-stack
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Card Stack
* @version: 1.1.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion, useReducedMotion } from "motion/react";
import Image from "next/image";
import { useState } from "react";
import { cn } from "@/lib/utils";
interface Specification {
label: string;
value: string;
}
interface Product {
id: string;
title: string;
subtitle: string;
description: string;
image: string;
specs: Specification[];
}
const products: Product[] = [
{
id: "instant-pay",
title: "Quick Pay",
subtitle: "Instant Transfers",
description:
"Move money in seconds with bank-grade security and zero surprises.",
image: "/undraw.svg",
specs: [
{ label: "Speed", value: "Instant" },
{ label: "Security", value: "256-bit" },
{ label: "Limit", value: "$50,000" },
{ label: "Fee", value: "0.5%" },
],
},
{
id: "crypto-pay",
title: "Crypto Pay",
subtitle: "Web3 Payments",
description:
"Accept crypto across every major chain with optimized gas routing.",
image:
"https://images.unsplash.com/photo-1527443224154-c4a3942d3acf?w=800&auto=format&fit=crop&q=80",
specs: [
{ label: "Network", value: "Multi-chain" },
{ label: "Gas", value: "Optimized" },
{ label: "Support", value: "24/7" },
{ label: "Security", value: "Top-tier" },
],
},
{
id: "business-pay",
title: "Business Pay",
subtitle: "Enterprise Solutions",
description:
"Built for high-volume teams with custom APIs and premium support.",
image:
"https://images.unsplash.com/photo-1544244015-0df4b3ffc6b0?w=800&auto=format&fit=crop&q=80",
specs: [
{ label: "Volume", value: "Unlimited" },
{ label: "API", value: "REST/SDK" },
{ label: "Support", value: "Premium" },
{ label: "Features", value: "Custom" },
],
},
{
id: "global-pay",
title: "Global Pay",
subtitle: "International Transfers",
description:
"Send to 180+ countries with real-time FX and same-day settlement.",
image:
"https://images.unsplash.com/photo-1629131726692-1accd0c53ce0?w=800&auto=format&fit=crop&q=80",
specs: [
{ label: "Countries", value: "180+" },
{ label: "FX Rate", value: "Real-time" },
{ label: "Speed", value: "Same-day" },
{ label: "Support", value: "Local" },
],
},
];
const CARD_WIDTH = 320;
const CARD_OVERLAP = 240;
interface CardProps {
product: Product;
index: number;
totalCards: number;
isExpanded: boolean;
reducedMotion: boolean;
}
const Card = ({
product,
index,
totalCards,
isExpanded,
reducedMotion,
}: CardProps) => {
const centerOffset = (totalCards - 1) * 5;
const defaultX = index * 10 - centerOffset;
const defaultY = index * 2;
const defaultRotate = index * 1.5;
const totalExpandedWidth =
CARD_WIDTH + (totalCards - 1) * (CARD_WIDTH - CARD_OVERLAP);
const expandedCenterOffset = totalExpandedWidth / 2;
const spreadX =
index * (CARD_WIDTH - CARD_OVERLAP) - expandedCenterOffset + CARD_WIDTH / 2;
const spreadRotate = index * 5 - (totalCards - 1) * 2.5;
const collapsedPose = {
x: defaultX,
y: defaultY,
rotate: reducedMotion ? 0 : defaultRotate,
scale: 1,
};
const expandedPose = {
x: spreadX,
y: 0,
rotate: reducedMotion ? 0 : spreadRotate,
scale: 1,
};
const isSvg = product.image.endsWith(".svg");
return (
{product.specs.map((spec) => (
{spec.value}
{spec.label}
))}
{product.title}
{product.subtitle}
{product.description}
);
};
interface CardStackProps {
className?: string;
}
export default function CardStackExample({ className }: CardStackProps) {
const [isExpanded, setIsExpanded] = useState(false);
const reducedMotion = useReducedMotion() ?? false;
const handleToggle = () => setIsExpanded((prev) => !prev);
return (
{products.map((product, index) => (
))}
);
}
```
---
# Carousel Cards
> Horizontally scrolling card carousel with ratings, badges and arrow navigation, built with React, Tailwind CSS and shadcn/ui primitives.
Source: https://kokonutui.com/docs/cards/carousel-cards
## Installation
```bash
npx shadcn@latest add @kokonutui/carousel-cards
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Carousel Cards
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { ChevronLeft, ChevronRight, Heart, Star } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import React from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardFooter } from "@/components/ui/card";
interface ExperienceItem {
id: string;
title: string;
image: string;
location: string;
price: number;
currency?: string;
rating?: number;
reviewCount?: number;
badge?: string;
date?: string;
}
interface ExperienceGridProps {
title: string;
items: ExperienceItem[];
viewAllHref?: string;
}
const sampleExperiences: ExperienceItem[] = [
{
id: "1",
title: "Become an Otaku Hottie with Megan Thee Stallion",
image: "https://images.unsplash.com/photo-1615571022219-eb45cf7faa9d",
location: "Los Angeles, United States",
price: 120,
currency: "β¬",
rating: 4.97,
reviewCount: 128,
badge: "Original",
date: "Closes May 21",
},
{
id: "2",
title: "Spend a Sunday Funday with Patrick Mahomes",
image: "https://images.unsplash.com/photo-1622127922040-13cab637ee78",
location: "Kansas City, United States",
price: 150,
currency: "β¬",
rating: 4.92,
reviewCount: 86,
badge: "Original",
date: "Closes Today",
},
{
id: "3",
title: "Celebrate with SEVENTEEN on their 10th anniversary",
image: "https://images.unsplash.com/photo-1534430480872-3498386e7856",
location: "Seoul, South Korea",
price: 200,
currency: "β¬",
rating: 4.98,
reviewCount: 254,
badge: "Original",
date: "Closed May 17",
},
{
id: "4",
title: "Learn the secrets of French pastry with nonnas",
image: "https://images.unsplash.com/photo-1604999333679-b86d54738315",
location: "Paris, France",
price: 70,
currency: "β¬",
rating: 4.97,
reviewCount: 112,
badge: "Original",
},
{
id: "5",
title: "Uncover the world of cabaret with a burlesque show",
image: "https://images.unsplash.com/photo-1552733407-5d5c46c3bb3b",
location: "Paris, France",
price: 92,
currency: "β¬",
rating: 4.9,
reviewCount: 78,
badge: "Original",
},
{
id: "6",
title: "The Super Powers of Art-family Game at the Louvre",
image: "https://images.unsplash.com/photo-1530789253388-582c481c54b0",
location: "Paris, France",
price: 90,
currency: "β¬",
rating: 4.98,
reviewCount: 146,
badge: "Original",
},
{
id: "7",
title: "Savor tasty vegan pastries with a plant-based pro",
image: "https://images.unsplash.com/photo-1608830597604-619220679440",
location: "Paris, France",
price: 75,
currency: "β¬",
rating: 4.95,
reviewCount: 92,
badge: "Original",
},
];
const popularExperiences: ExperienceItem[] = [
{
id: "p1",
title: "Learn to bake the French Croissant",
image: "https://images.unsplash.com/photo-1555507036-ab1f4038808a",
location: "Paris, France",
price: 95,
currency: "β¬",
rating: 4.95,
reviewCount: 218,
badge: "Popular",
},
{
id: "p2",
title: "Seek out hidden speakeasy bars in the city",
image: "https://images.unsplash.com/photo-1470337458703-46ad1756a187",
location: "Paris, France",
price: 74,
currency: "β¬",
rating: 4.9,
reviewCount: 165,
badge: "Popular",
},
{
id: "p3",
title: "Versailles Food and Palace Bike Tour",
image: "https://images.unsplash.com/photo-1555507036-ab1f4038808a",
location: "Versailles, France",
price: 122,
currency: "β¬",
rating: 4.97,
reviewCount: 89,
badge: "Popular",
},
{
id: "p4",
title: "Haunted Paris Tour - Ghosts, Legends, True Crime",
image: "https://images.unsplash.com/photo-1549144511-f099e773c147",
location: "Paris, France",
price: 25,
currency: "β¬",
rating: 4.98,
reviewCount: 345,
badge: "Popular",
},
{
id: "p5",
title: "Learn to make the French macarons with a chef",
image: "https://images.unsplash.com/photo-1558326567-98ae2405596b",
location: "Paris, France",
price: 110,
currency: "β¬",
rating: 4.95,
reviewCount: 203,
badge: "Popular",
},
{
id: "p6",
title: "No Diet Club - Best food tour in Le Marais",
image: "https://images.unsplash.com/photo-1414235077428-338989a2e8c0",
location: "Paris, France",
price: 65,
currency: "β¬",
rating: 4.92,
reviewCount: 178,
badge: "5 spots left",
},
{
id: "p7",
title: "Soak up the nightlife of Paris",
image: "https://images.unsplash.com/photo-1546636889-ba9fdd63583e",
location: "Paris, France",
price: 20,
currency: "β¬",
rating: 4.94,
reviewCount: 112,
badge: "Popular",
},
];
const ExperienceCard = ({ experience }: { experience: ExperienceItem }) => (
Add to favorites
{experience.badge && (
{experience.badge}
)}
{experience.title}
{experience.location}
{experience.date && (
{experience.date}
)}
{experience.rating && (
{experience.rating}
)}
{experience.reviewCount && (
{experience.rating && "Β·"}
{experience.reviewCount > 0 ? ` (${experience.reviewCount})` : ""}
)}
{experience.currency || "β¬"} {experience.price} / guest
);
const ExperienceSection = ({
title,
items,
viewAllHref = "#",
}: ExperienceGridProps) => {
const scrollContainer = React.useRef(null);
const handleScrollLeft = () => {
if (scrollContainer.current) {
scrollContainer.current.scrollBy({
left: -320,
behavior: "smooth",
});
}
};
const handleScrollRight = () => {
if (scrollContainer.current) {
scrollContainer.current.scrollBy({ left: 320, behavior: "smooth" });
}
};
return (
{title}
Scroll left
Scroll right
Show all
{items.map((item) => (
))}
);
};
export default function CarouselCards() {
return (
);
}
```
---
# Currency Transfer
> Multi-step currency exchange card with animated progress and SVG checkmark confirmation, built with React, Tailwind CSS and Motion animations.
Source: https://kokonutui.com/docs/cards/currency-transfer
## Installation
```bash
npx shadcn@latest add @kokonutui/currency-transfer
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Currency Transfer
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import {
ArrowDownIcon,
ArrowUpDown,
ArrowUpIcon,
Check,
InfoIcon,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
interface CheckmarkProps {
size?: number;
strokeWidth?: number;
color?: string;
className?: string;
}
const draw = {
hidden: { pathLength: 0, opacity: 0 },
visible: (i: number) => ({
pathLength: 1,
opacity: 1,
transition: {
pathLength: {
delay: i * 0.2,
type: "spring",
duration: 1.5,
bounce: 0.2,
ease: [0.22, 1, 0.36, 1],
},
opacity: { delay: i * 0.2, duration: 0.3 },
},
}),
};
export function Checkmark({
size = 100,
strokeWidth = 2,
color = "currentColor",
className = "",
}: CheckmarkProps) {
return (
Animated Checkmark
);
}
export default function CurrencyTransfer() {
const [isCompleted, setIsCompleted] = useState(false);
const transactionId = "TXN-DAB3UL494";
useEffect(() => {
const timer = setTimeout(() => {
setIsCompleted(true);
}, 1500);
return () => clearTimeout(timer);
}, []);
return (
{isCompleted ? (
) : (
)}
{isCompleted ? (
Transfer Completed
) : (
Transfer in Progress
)}
{isCompleted ? (
Transaction ID: {transactionId}
) : (
Processing Transaction...
)}
From
$
500.00 USD
Chase Bank β’β’β’β’4589
To
β¬
460.00 EUR
Deutsche Bank β’β’β’β’7823
{isCompleted ? (
Exchange Rate: 1 USD = 0.92 EUR
) : (
Calculating exchange rate...
)}
{isCompleted
? "Rate updated at 10:45 AM"
: "Please wait..."}
);
}
```
---
# Liquid Glass
> Apple-inspired liquid glass card and buttons using SVG displacement filters for a refractive effect, built with React and Tailwind CSS.
Source: https://kokonutui.com/docs/cards/liquid-glass-card
## Installation
```bash
npx shadcn@latest add @kokonutui/liquid-glass-card
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Liquid Glass Card - Optimized with Shadcn UI
* @version: 2.0.0
* @date: 2025-10-11
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { cva, type VariantProps } from "class-variance-authority";
import {
ArrowLeft,
ArrowRight,
MoreHorizontal,
Pause,
Play,
} from "lucide-react";
import Image from "next/image";
import React from "react";
import { Button, type ButtonProps } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
// Constants for better maintainability
const GLASS_SHADOW_LIGHT =
"shadow-[0_0_6px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3px_rgba(0,0,0,0.9),inset_-3px_-3px_0.5px_-3px_rgba(0,0,0,0.85),inset_1px_1px_1px_-0.5px_rgba(0,0,0,0.6),inset_-1px_-1px_1px_-0.5px_rgba(0,0,0,0.6),inset_0_0_6px_6px_rgba(0,0,0,0.12),inset_0_0_2px_2px_rgba(0,0,0,0.06),0_0_12px_rgba(255,255,255,0.15)]";
const GLASS_SHADOW_DARK =
"dark:shadow-[0_0_8px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3.5px_rgba(255,255,255,0.09),inset_-3px_-3px_0.5px_-3.5px_rgba(255,255,255,0.85),inset_1px_1px_1px_-0.5px_rgba(255,255,255,0.6),inset_-1px_-1px_1px_-0.5px_rgba(255,255,255,0.6),inset_0_0_6px_6px_rgba(255,255,255,0.12),inset_0_0_2px_2px_rgba(255,255,255,0.06),0_0_12px_rgba(0,0,0,0.15)]";
const GLASS_SHADOW = `${GLASS_SHADOW_LIGHT} ${GLASS_SHADOW_DARK}`;
const DEFAULT_GLASS_FILTER_SCALE = 30;
const BUTTON_GLASS_FILTER_SCALE = 70;
// Shared glass filter component
interface GlassFilterProps {
id: string;
scale?: number;
}
const GlassFilter = React.memo(
({ id, scale = DEFAULT_GLASS_FILTER_SCALE }: GlassFilterProps) => (
Glass Effect Filter
)
);
GlassFilter.displayName = "GlassFilter";
// Liquid Button - extends shadcn Button with glass effect
const liquidButtonVariants = cva(
"relative transition-transform duration-200 motion-reduce:transition-none",
{
variants: {
liquidVariant: {
default:
"active:scale-[0.97] motion-reduce:active:scale-100 motion-reduce:hover:scale-100 [@media(hover:hover)]:hover:scale-105",
none: "",
},
},
defaultVariants: {
liquidVariant: "default",
},
}
);
export type LiquidButtonProps = ButtonProps &
VariantProps;
function LiquidButton({
className,
liquidVariant = "default",
children,
...props
}: LiquidButtonProps) {
const filterId = React.useId();
return (
<>
{children}
>
);
}
// Liquid Glass Card - extends shadcn Card with glass effect
const liquidGlassCardVariants = cva(
"group relative overflow-hidden bg-background/20 backdrop-blur-[2px]",
{
variants: {
glassSize: {
sm: "p-4",
default: "p-6",
lg: "p-8",
},
},
defaultVariants: {
glassSize: "default",
},
}
);
export type LiquidGlassCardProps = React.HTMLAttributes &
VariantProps & {
glassEffect?: boolean;
};
function LiquidGlassCard({
className,
glassSize,
glassEffect = true,
children,
...props
}: LiquidGlassCardProps) {
const filterId = React.useId();
return (
{glassEffect && (
<>
>
)}
{children}
);
}
// Demo: Music Player Card
const TOTAL_DURATION = 45;
const VOLUME_BAR_COUNT = 8;
const SEEK_JUMP_SECONDS = 5;
const TIMER_INTERVAL_MS = 1000;
const STATIC_BAR_HEIGHT = "6px";
const MIN_TIME = 0;
const BAR_DELAY_INCREMENT = 0.1;
const PROGRESS_PERCENTAGE_MULTIPLIER = 100;
const formatTime = (timeInSeconds: number): string => {
const minutes = Math.floor(timeInSeconds / 60);
const seconds = Math.floor(timeInSeconds % 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
};
interface VolumeBarsProps {
isPlaying: boolean;
}
const VolumeBars = React.memo(({ isPlaying }: VolumeBarsProps) => {
const bars = Array.from({ length: VOLUME_BAR_COUNT }, (_, i) => ({
id: `bar-${i}`,
delay: i * BAR_DELAY_INCREMENT,
}));
return (
);
});
VolumeBars.displayName = "VolumeBars";
interface ProgressBarProps {
currentTime: number;
totalDuration: number;
onSeek: (newTime: number) => void;
}
const ProgressBar = React.memo(
({ currentTime, totalDuration, onSeek }: ProgressBarProps) => {
const progress =
(currentTime / totalDuration) * PROGRESS_PERCENTAGE_MULTIPLIER;
const handleClick = (e: React.MouseEvent) => {
const bar = e.currentTarget;
const rect = bar.getBoundingClientRect();
const x = e.clientX - rect.left;
const percent = x / rect.width;
const newTime = Math.min(
Math.max(MIN_TIME, percent * totalDuration),
totalDuration
);
onSeek(newTime);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case "ArrowRight":
e.preventDefault();
onSeek(Math.min(currentTime + SEEK_JUMP_SECONDS, totalDuration));
break;
case "ArrowLeft":
e.preventDefault();
onSeek(Math.max(currentTime - SEEK_JUMP_SECONDS, MIN_TIME));
break;
case "Home":
e.preventDefault();
onSeek(MIN_TIME);
break;
case "End":
e.preventDefault();
onSeek(totalDuration);
break;
default:
break;
}
};
return (
<>
{formatTime(currentTime)}
{formatTime(totalDuration)}
>
);
}
);
ProgressBar.displayName = "ProgressBar";
export function NotificationCenter() {
const [isPlaying, setIsPlaying] = React.useState(true);
const [currentTime, setCurrentTime] = React.useState(MIN_TIME);
const rootRef = React.useRef(null);
const [isVisible, setIsVisible] = React.useState(true);
// Only tick while on screen β an off-screen interval keeps waking the main
// thread and re-rendering for a component nobody can see.
React.useEffect(() => {
const element = rootRef.current;
if (!element) {
return;
}
const observer = new IntersectionObserver(
([entry]) => setIsVisible(entry.isIntersecting),
{ rootMargin: "100px" }
);
observer.observe(element);
return () => observer.disconnect();
}, []);
React.useEffect(() => {
if (!(isPlaying && isVisible)) {
return;
}
const intervalId = setInterval(() => {
setCurrentTime((prev) =>
prev + 1 >= TOTAL_DURATION ? TOTAL_DURATION : prev + 1
);
}, TIMER_INTERVAL_MS);
return () => clearInterval(intervalId);
}, [isPlaying, isVisible]);
React.useEffect(() => {
if (currentTime >= TOTAL_DURATION) {
setIsPlaying(false);
}
}, [currentTime]);
const handlePlayPause = () => {
setIsPlaying((prev) => !prev);
};
const handleSeek = (newTime: number) => {
setCurrentTime(newTime);
if (newTime < TOTAL_DURATION && !isPlaying) {
setIsPlaying(true);
}
};
return (
);
}
export { LiquidButton, LiquidGlassCard };
export default NotificationCenter;
```
---
# Mouse Effect Card
> Interactive card with an animated dot pattern that repels away from the cursor, powered by React, Tailwind CSS and Motion spring physics.
Source: https://kokonutui.com/docs/cards/mouse-effect-card
## Installation
```bash
npx shadcn@latest add @kokonutui/mouse-effect-card
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Mouse Effect Card - Interactive card with animated dot pattern that responds to mouse movement
* @version: 1.0.0
* @date: 2025-01-30
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion, useMotionValue, useSpring, useTransform } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
const SPRING_CONFIG = { stiffness: 300, damping: 30, mass: 0.5 };
const OPACITY_DURATION_BASE = 0.8;
const OPACITY_DURATION_VARIATION = 0.2;
const OPACITY_EASE = [0.4, 0, 0.2, 1] as const;
const OPACITY_DELAY_CYCLE = 1.5;
const OPACITY_DELAY_STEP = 0.02;
const MIN_OPACITY_MULTIPLIER = 0.5;
const MAX_OPACITY_MULTIPLIER = 1.5;
const MIN_OPACITY_FALLBACK = 0.3;
const PROXIMITY_MULTIPLIER = 1.2;
const PROXIMITY_OPACITY_BOOST = 0.8;
export interface MouseEffectCardProps {
className?: string;
children?: React.ReactNode;
dotSize?: number;
dotSpacing?: number;
repulsionRadius?: number;
repulsionStrength?: number;
title?: string;
subtitle?: string;
topText?: string;
topSubtext?: string;
primaryCtaText?: string;
primaryCtaUrl?: string;
secondaryCtaText?: string;
secondaryCtaUrl?: string;
footerText?: string;
ariaLabel?: string;
}
interface Dot {
id: string;
x: number;
y: number;
baseX: number;
baseY: number;
opacity: number;
}
interface DotComponentProps {
dot: Dot;
index: number;
dotSize: number;
mouseX: ReturnType>;
mouseY: ReturnType>;
repulsionRadius: number;
repulsionStrength: number;
}
function calculateDistance(
x1: number,
y1: number,
x2: number,
y2: number
): number {
const dx = x1 - x2;
const dy = y1 - y2;
return Math.sqrt(dx * dx + dy * dy);
}
function generateDots(width: number, height: number, spacing: number): Dot[] {
const dots: Dot[] = [];
const cols = Math.ceil(width / spacing);
const rows = Math.ceil(height / spacing);
const centerX = width / 2;
const centerY = height / 2;
const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);
for (let row = 0; row <= rows; row++) {
for (let col = 0; col <= cols; col++) {
const x = col * spacing;
const y = row * spacing;
// Calculate distance from center
const dx = x - centerX;
const dy = y - centerY;
const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
// Calculate edge factor (0 at edges, 1 at center)
const edgeFactor = Math.min(distanceFromCenter / (maxDistance * 0.7), 1);
// Skip dots near edges with probability based on distance
if (Math.random() > edgeFactor) {
continue;
}
const pattern = (row + col) % 3;
const baseOpacities = [0.3, 0.5, 0.7];
const opacity = baseOpacities[pattern] * edgeFactor;
dots.push({
id: `dot-${row}-${col}`,
x,
y,
baseX: x,
baseY: y,
opacity,
});
}
}
return dots;
}
function DotComponent({
dot,
index,
dotSize,
mouseX,
mouseY,
repulsionRadius,
repulsionStrength,
}: DotComponentProps) {
const posX = useTransform([mouseX, mouseY], () => {
const mx = mouseX.get();
const my = mouseY.get();
if (!(Number.isFinite(mx) && Number.isFinite(my))) {
return 0;
}
const dx = dot.baseX - mx;
const dy = dot.baseY - my;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < repulsionRadius) {
const force = (1 - distance / repulsionRadius) * repulsionStrength;
const angle = Math.atan2(dy, dx);
return Math.cos(angle) * force;
}
return 0;
});
const posY = useTransform([mouseX, mouseY], () => {
const mx = mouseX.get();
const my = mouseY.get();
if (!(Number.isFinite(mx) && Number.isFinite(my))) {
return 0;
}
const dx = dot.baseX - mx;
const dy = dot.baseY - my;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < repulsionRadius) {
const force = (1 - distance / repulsionRadius) * repulsionStrength;
const angle = Math.atan2(dy, dx);
return Math.sin(angle) * force;
}
return 0;
});
const opacityBoost = useTransform([mouseX, mouseY], () => {
const mx = mouseX.get();
const my = mouseY.get();
if (!(Number.isFinite(mx) && Number.isFinite(my))) return 0;
const distance = calculateDistance(dot.baseX, dot.baseY, mx, my);
const maxDistance = repulsionRadius * PROXIMITY_MULTIPLIER;
if (distance < maxDistance) {
const proximityFactor = 1 - distance / maxDistance;
return proximityFactor * PROXIMITY_OPACITY_BOOST;
}
return 0;
});
const x = useSpring(posX, SPRING_CONFIG);
const y = useSpring(posY, SPRING_CONFIG);
const baseMinOpacity = Math.max(
dot.opacity * MIN_OPACITY_MULTIPLIER,
MIN_OPACITY_FALLBACK
);
const baseMaxOpacity = Math.min(dot.opacity * MAX_OPACITY_MULTIPLIER, 1);
const minOpacityWithBoost = useTransform(opacityBoost, (boost) =>
Math.min(baseMinOpacity + boost, 1)
);
const delay = (index * OPACITY_DELAY_STEP) % OPACITY_DELAY_CYCLE;
return (
);
}
export default function MouseEffectCard({
className,
children,
dotSize = 2,
dotSpacing = 16,
repulsionRadius = 80,
repulsionStrength = 20,
title = "Acme",
subtitle = "Build interfaces with interactive patterns",
topText = "Case Study",
topSubtext = "Discover something new",
primaryCtaText = "Get Started",
primaryCtaUrl = "#",
secondaryCtaText = "View Docs",
secondaryCtaUrl = "#",
footerText = "We do it all",
ariaLabel,
}: MouseEffectCardProps) {
const innerContainerRef = useRef(null);
const mouseX = useMotionValue(Number.POSITIVE_INFINITY);
const mouseY = useMotionValue(Number.POSITIVE_INFINITY);
const [dots, setDots] = useState([]);
useEffect(() => {
const updateDots = () => {
if (!innerContainerRef.current) return;
const rect = innerContainerRef.current.getBoundingClientRect();
const newDots = generateDots(rect.width, rect.height, dotSpacing);
setDots(newDots);
};
updateDots();
const resizeObserver = new ResizeObserver(updateDots);
if (innerContainerRef.current) {
resizeObserver.observe(innerContainerRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, [dotSpacing]);
const handleMouseMove = (e: React.MouseEvent) => {
if (!innerContainerRef.current) return;
const rect = innerContainerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
mouseX.set(x);
mouseY.set(y);
};
const handleMouseLeave = () => {
mouseX.set(Number.POSITIVE_INFINITY);
mouseY.set(Number.POSITIVE_INFINITY);
};
const handleFocus = () => {
if (!innerContainerRef.current) return;
const rect = innerContainerRef.current.getBoundingClientRect();
mouseX.set(rect.width / 2);
mouseY.set(rect.height / 2);
};
const handleBlur = () => {
mouseX.set(Number.POSITIVE_INFINITY);
mouseY.set(Number.POSITIVE_INFINITY);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (!innerContainerRef.current) return;
const rect = innerContainerRef.current.getBoundingClientRect();
const step = Math.min(rect.width, rect.height) * 0.2;
const currentX = Number.isFinite(mouseX.get())
? mouseX.get()
: rect.width / 2;
const currentY = Number.isFinite(mouseY.get())
? mouseY.get()
: rect.height / 2;
switch (e.key) {
case "ArrowUp":
e.preventDefault();
mouseY.set(Math.max(0, currentY - step));
mouseX.set(currentX);
break;
case "ArrowDown":
e.preventDefault();
mouseY.set(Math.min(rect.height, currentY + step));
mouseX.set(currentX);
break;
case "ArrowLeft":
e.preventDefault();
mouseX.set(Math.max(0, currentX - step));
mouseY.set(currentY);
break;
case "ArrowRight":
e.preventDefault();
mouseX.set(Math.min(rect.width, currentX + step));
mouseY.set(currentY);
break;
}
};
return (
{dots.map((dot, index) => (
))}
{topText && (
{topText}
{topSubtext && (
{topSubtext}
)}
)}
{(subtitle || children) && (
)}
{footerText && (
)}
);
}
```
---
# Spotlight Cards
> Feature card grid with ambient aurora glow, magnetic 3D tilt and focus dimming on hover, built with React, Tailwind CSS and Motion springs.
Source: https://kokonutui.com/docs/cards/spotlight-cards
## Installation
```bash
npx shadcn@latest add @kokonutui/spotlight-cards
```
## Source
```tsx
"use client";
/**
* @author dorianbaffier
* @description Feature grid with aurora ambient, magnetic 3D tilt, and focus-dim siblings.
* @version 2.0.0
* @date 2025-02-20
* @license MIT
* @website https://kokonutui.com
* @github https://github.com/kokonut-labs/kokonutui
*/
import type { LucideIcon } from "lucide-react";
import { Cloud, Code, Cpu, Globe, Lock, Zap } from "lucide-react";
import { motion, useMotionValue, useSpring, useTransform } from "motion/react";
import { useRef, useState } from "react";
import { cn } from "@/lib/utils";
// βββ Constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const TILT_MAX = 9;
const TILT_SPRING = { stiffness: 300, damping: 28 } as const;
const GLOW_SPRING = { stiffness: 180, damping: 22 } as const;
// βββ Data ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface SpotlightItem {
icon: LucideIcon;
title: string;
description: string;
color: string;
}
const DEFAULT_ITEMS: SpotlightItem[] = [
{
icon: Zap,
title: "Instant",
description:
"Sub-100ms latency on every request, globally distributed across every region.",
color: "#f59e0b",
},
{
icon: Lock,
title: "Secure",
description:
"Zero-trust by default. SOC 2 certified with end-to-end encryption throughout.",
color: "#60a5fa",
},
{
icon: Globe,
title: "Global",
description:
"Edge-deployed to 300+ locations. Your users always hit a nearby server.",
color: "#34d399",
},
{
icon: Code,
title: "Developer first",
description:
"Type-safe SDKs in five languages, a complete REST API, and honest docs.",
color: "#a78bfa",
},
{
icon: Cpu,
title: "Scalable",
description:
"From side project to Series B without touching your infrastructure config.",
color: "#38bdf8",
},
{
icon: Cloud,
title: "Serverless",
description:
"No servers to provision, patch, or babysit. Just deploy and move on.",
color: "#f472b6",
},
];
// βββ Card ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
interface CardProps {
item: SpotlightItem;
dimmed: boolean;
onHoverStart: () => void;
onHoverEnd: () => void;
}
function Card({ item, dimmed, onHoverStart, onHoverEnd }: CardProps) {
const Icon = item.icon;
const cardRef = useRef(null);
const normX = useMotionValue(0.5);
const normY = useMotionValue(0.5);
const rawRotateX = useTransform(normY, [0, 1], [TILT_MAX, -TILT_MAX]);
const rawRotateY = useTransform(normX, [0, 1], [-TILT_MAX, TILT_MAX]);
const rotateX = useSpring(rawRotateX, TILT_SPRING);
const rotateY = useSpring(rawRotateY, TILT_SPRING);
const glowOpacity = useSpring(0, GLOW_SPRING);
const handleMouseMove = (e: React.MouseEvent) => {
const el = cardRef.current;
if (!el) {
return;
}
const rect = el.getBoundingClientRect();
normX.set((e.clientX - rect.left) / rect.width);
normY.set((e.clientY - rect.top) / rect.height);
};
const handleMouseEnter = () => {
glowOpacity.set(1);
onHoverStart();
};
const handleMouseLeave = () => {
normX.set(0.5);
normY.set(0.5);
glowOpacity.set(0);
onHoverEnd();
};
return (
{/* Static accent tint β always visible */}
{/* Hover glow layer */}
{/* Shimmer sweep */}
{/* Icon badge */}
{/* Text */}
{item.title}
{item.description}
{/* Accent bottom line */}
);
}
Card.displayName = "Card";
// βββ Main export ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export interface SpotlightCardsProps {
items?: SpotlightItem[];
eyebrow?: string;
heading?: string;
className?: string;
}
export default function SpotlightCards({
items = DEFAULT_ITEMS,
eyebrow = "Features",
heading = "Everything you need",
className,
}: SpotlightCardsProps) {
const [hoveredTitle, setHoveredTitle] = useState(null);
return (
{/* Dot grid β light mode only */}
{/* Header */}
{/* Card grid */}
{items.map((item) => (
setHoveredTitle(null)}
onHoverStart={() => setHoveredTitle(item.title)}
/>
))}
);
}
```
---
# X Card
> X (Twitter) style post card with author details, verified badge, reply thread and hover gradient, built with React and Tailwind CSS.
Source: https://kokonutui.com/docs/cards/tweet-card
## Installation
```bash
npx shadcn@latest add @kokonutui/tweet-card
```
## Source
```tsx
import { VerifiedIcon } from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
/**
* @author: @dorianbaffier
* @description: Tweet Card
* @version: 1.0.0
* @date: 2025-10-01
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
type ReplyProps = {
authorName: string;
authorHandle: string;
authorImage: string;
content: string;
isVerified?: boolean;
timestamp: string;
};
type TweetCardProps = {
authorName?: string;
authorHandle?: string;
authorImage?: string;
content?: string[];
isVerified?: boolean;
timestamp?: string;
href?: string;
reply?: ReplyProps;
className?: string;
};
export default function TweetCard({
authorName = "Dorian",
authorHandle = "dorianbaffier",
authorImage = "https://pbs.twimg.com/profile_images/1992215290936205312/N_EuwLUO_400x400.jpg",
content = [
"All components from KokonutUI can now be open in @v0 π",
"1. Click on 'Open in V0'",
"2. Customize with prompts",
"3. Deploy to your app",
],
isVerified = true,
timestamp = "Jan 18, 2025",
href = "#",
reply = {
authorName: "shadcn",
authorHandle: "shadcn",
authorImage:
"https://pbs.twimg.com/profile_images/1593304942210478080/TUYae5z7_400x400.jpg",
content: "Awesome.",
isVerified: true,
timestamp: "Jan 18",
},
className,
}: TweetCardProps) {
return (
{authorName}
{isVerified && (
)}
@{authorHandle}
X
{content.map((item, index) => (
{item}
))}
{timestamp}
{reply && (
{reply.authorName}
{reply.isVerified && (
)}
@{reply.authorHandle}
Β·
{reply.timestamp}
{reply.content}
)}
);
}
```
---
# Action Search Bar
> Search bar with debounced input, shortcut hints, and an animated action suggestions dropdown, built with React, Motion, and Tailwind CSS.
Source: https://kokonutui.com/docs/navigation/action-search-bar
## Installation
```bash
npx shadcn@latest add @kokonutui/action-search-bar
```
## Source
```tsx
"use client";
/**
* @author: @kokonutui
* @description: A modern search bar component with action buttons and suggestions
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import {
AudioLines,
BarChart2,
LayoutGrid,
PlaneTakeoff,
Search,
Send,
Video,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Input } from "@/components/ui/input";
import useDebounce from "@/hooks/use-debounce";
interface Action {
id: string;
label: string;
icon: React.ReactNode;
description?: string;
short?: string;
end?: string;
}
interface SearchResult {
actions: Action[];
}
const ANIMATION_VARIANTS = {
container: {
hidden: { opacity: 0, height: 0 },
show: {
opacity: 1,
height: "auto",
transition: {
height: { duration: 0.4 },
staggerChildren: 0.1,
},
},
exit: {
opacity: 0,
height: 0,
transition: {
height: { duration: 0.3 },
opacity: { duration: 0.2 },
},
},
},
item: {
hidden: { opacity: 0, y: 20 },
show: {
opacity: 1,
y: 0,
transition: { duration: 0.3 },
},
exit: {
opacity: 0,
y: -10,
transition: { duration: 0.2 },
},
},
} as const;
const allActionsSample = [
{
id: "1",
label: "Book tickets",
icon: ,
description: "Operator",
short: "βK",
end: "Agent",
},
{
id: "2",
label: "Summarize",
icon: ,
description: "gpt-5",
short: "βcmd+p",
end: "Command",
},
{
id: "3",
label: "Screen Studio",
icon: ,
description: "Claude 4.1",
short: "",
end: "Application",
},
{
id: "4",
label: "Talk to Jarvis",
icon: ,
description: "gpt-5 voice",
short: "",
end: "Active",
},
{
id: "5",
label: "Kokonut UI - Pro",
icon: ,
description: "Components",
short: "",
end: "Link",
},
];
function ActionSearchBar({
actions = allActionsSample,
defaultOpen = false,
}: {
actions?: Action[];
defaultOpen?: boolean;
}) {
const [query, setQuery] = useState("");
const [result, setResult] = useState(null);
const [isFocused, setIsFocused] = useState(defaultOpen);
const [isTyping, setIsTyping] = useState(false);
const [selectedAction, setSelectedAction] = useState(null);
const [activeIndex, setActiveIndex] = useState(-1);
const debouncedQuery = useDebounce(query, 200);
const filteredActions = useMemo(() => {
if (!debouncedQuery) return actions;
const normalizedQuery = debouncedQuery.toLowerCase().trim();
return actions.filter((action) => {
const searchableText =
`${action.label} ${action.description || ""}`.toLowerCase();
return searchableText.includes(normalizedQuery);
});
}, [debouncedQuery, actions]);
useEffect(() => {
if (!isFocused) {
setResult(null);
setActiveIndex(-1);
return;
}
setResult({ actions: filteredActions });
setActiveIndex(-1);
}, [filteredActions, isFocused]);
const handleInputChange = useCallback(
(e: React.ChangeEvent) => {
setQuery(e.target.value);
setIsTyping(true);
setActiveIndex(-1);
},
[]
);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (!result?.actions.length) return;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setActiveIndex((prev) =>
prev < result.actions.length - 1 ? prev + 1 : 0
);
break;
case "ArrowUp":
e.preventDefault();
setActiveIndex((prev) =>
prev > 0 ? prev - 1 : result.actions.length - 1
);
break;
case "Enter":
e.preventDefault();
if (activeIndex >= 0 && result.actions[activeIndex]) {
setSelectedAction(result.actions[activeIndex]);
}
break;
case "Escape":
setIsFocused(false);
setActiveIndex(-1);
break;
}
},
[result?.actions, activeIndex]
);
const handleActionClick = useCallback((action: Action) => {
setSelectedAction(action);
}, []);
const handleFocus = useCallback(() => {
setSelectedAction(null);
setIsFocused(true);
setActiveIndex(-1);
}, []);
const handleBlur = useCallback(() => {
setTimeout(() => {
setIsFocused(false);
setActiveIndex(-1);
}, 200);
}, []);
return (
{isFocused && result && !selectedAction && (
{result.actions.map((action) => (
handleActionClick(action)}
role="option"
variants={ANIMATION_VARIANTS.item}
>
{action.icon}
{action.label}
{action.description && (
{action.description}
)}
{action.short && (
{action.short}
)}
{action.end && (
{action.end}
)}
))}
Press βK to open commands
ESC to cancel
)}
);
}
export default ActionSearchBar;
```
---
# Morphic Navbar
> Navigation bar whose active link morphs into a rounded pill with smooth CSS transitions, built with React, Next.js Link, and Tailwind CSS.
Source: https://kokonutui.com/docs/navigation/morphic-navbar
## Installation
```bash
npx shadcn@latest add @kokonutui/morphic-navbar
```
## Source
```tsx
"use client";
import clsx from "clsx";
import Link from "next/link";
import { useState } from "react";
interface NavItem {
name: string;
}
interface MorphicNavbarProps {
items?: Record;
defaultPath?: string;
className?: string;
}
const DEFAULT_NAV_ITEMS: Record = {
"/": { name: "home" },
"/works": { name: "works" },
"/blog": { name: "blog" },
"/about": { name: "about" },
};
export function MorphicNavbar({
items = DEFAULT_NAV_ITEMS,
defaultPath = "/",
className,
}: MorphicNavbarProps) {
const [activePath, setActivePath] = useState(defaultPath);
const isActiveLink = (path: string) => {
if (path === "/") {
return activePath === "/";
}
return activePath.startsWith(path);
};
return (
{Object.entries(items).map(([path, { name }], index, array) => {
const isActive = isActiveLink(path);
const isFirst = index === 0;
const isLast = index === array.length - 1;
const prevPath = index > 0 ? array[index - 1][0] : null;
const nextPath =
index < array.length - 1 ? array[index + 1][0] : null;
return (
setActivePath(path)}
>
{name}
);
})}
);
}
export default MorphicNavbar;
```
---
# Profile Dropdown
> User profile dropdown menu with avatar, subscription and model details, and quick action links, built with React, shadcn/ui, and Tailwind CSS.
Source: https://kokonutui.com/docs/navigation/profile-dropdown
## Installation
```bash
npx shadcn@latest add @kokonutui/profile-dropdown
```
## Source
```tsx
"use client";
import { CreditCard, FileText, LogOut, Settings, User } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import * as React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import Gemini from "../icons/gemini";
interface Profile {
name: string;
email: string;
avatar: string;
subscription?: string;
model?: string;
}
interface MenuItem {
label: string;
value?: string;
href: string;
icon: React.ReactNode;
external?: boolean;
}
const SAMPLE_PROFILE_DATA: Profile = {
name: "Eugene An",
email: "eugene@kokonutui.com",
avatar:
"https://ferf1mheo22r9ira.public.blob.vercel-storage.com/profile-mjss82WnWBRO86MHHGxvJ2TVZuyrDv.jpeg",
subscription: "PRO",
model: "Gemini 2.0 Flash",
};
interface ProfileDropdownProps extends React.HTMLAttributes {
data?: Profile;
showTopbar?: boolean;
}
export default function ProfileDropdown({
data = SAMPLE_PROFILE_DATA,
className,
...props
}: ProfileDropdownProps) {
const [isOpen, setIsOpen] = React.useState(false);
const menuItems: MenuItem[] = [
{
label: "Profile",
href: "#",
icon: ,
},
{
label: "Model",
value: data.model,
href: "#",
icon: ,
},
{
label: "Subscription",
value: data.subscription,
href: "#",
icon: ,
},
{
label: "Settings",
href: "#",
icon: ,
},
{
label: "Terms & Policies",
href: "#",
icon: ,
external: true,
},
];
return (
{/* Bending line indicator on the right */}
{menuItems.map((item) => (
{item.icon}
{item.label}
{item.value && (
{item.value}
)}
))}
Sign Out
);
}
```
---
# Smooth Drawer
> Bottom drawer with spring-based slide-in and staggered content reveal, built on Vaul with React, Motion, and Tailwind CSS.
Source: https://kokonutui.com/docs/navigation/smooth-drawer
## Installation
```bash
npx shadcn@latest add @kokonutui/smooth-drawer
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Smooth Drawer
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { Fingerprint } from "lucide-react";
import { motion } from "motion/react";
import Image from "next/image";
import Link from "next/link";
import type * as React from "react";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
interface PriceTagProps {
price: number;
discountedPrice: number;
}
function PriceTag({ price, discountedPrice }: PriceTagProps) {
return (
${discountedPrice}
${price}
Lifetime access
One-time payment
);
}
interface DrawerDemoProps extends React.HTMLAttributes {
title?: string;
description?: string;
primaryButtonText?: string;
secondaryButtonText?: string;
onPrimaryAction?: () => void;
onSecondaryAction?: () => void;
price?: number;
discountedPrice?: number;
}
const drawerVariants = {
hidden: {
y: "100%",
opacity: 0,
rotateX: 5,
transition: {
type: "spring",
stiffness: 300,
damping: 30,
},
},
visible: {
y: 0,
opacity: 1,
rotateX: 0,
transition: {
type: "spring",
stiffness: 300,
damping: 30,
mass: 0.8,
staggerChildren: 0.07,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: {
y: 20,
opacity: 0,
transition: {
type: "spring",
stiffness: 300,
damping: 30,
},
},
visible: {
y: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 300,
damping: 30,
mass: 0.8,
},
},
};
export default function SmoothDrawer({
title = "KokonutUI - Pro",
description = "100+ collection of UI Components and templates built for React, Next.js, and Tailwind CSS. Spend no time on design and focus on shipping.",
primaryButtonText = "Buy Now",
secondaryButtonText = "Maybe Later",
onSecondaryAction,
price = 169,
discountedPrice = 99,
}: DrawerDemoProps) {
const handleSecondaryClick = () => {
onSecondaryAction?.();
};
return (
Open Drawer
{title}
{description}
{primaryButtonText}
{secondaryButtonText}
);
}
```
---
# Smooth Tab
> Animated tab switcher with a sliding active indicator and smooth card content transitions, built with React, Motion, and Tailwind CSS.
Source: https://kokonutui.com/docs/navigation/smooth-tab
## Installation
```bash
npx shadcn@latest add @kokonutui/smooth-tab
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Smooth Tab
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import type { LucideIcon } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import * as React from "react";
import { cn } from "@/lib/utils";
interface TabItem {
id: string;
title: string;
description?: string;
icon?: LucideIcon;
content?: React.ReactNode;
cardContent?: React.ReactNode;
color: string;
}
const WaveformPath = () => (
);
function TabCardContent({
title,
description,
fillClass,
}: {
title: string;
description: string;
fillClass: string;
}) {
return (
);
}
const DEFAULT_TABS: TabItem[] = [
{
id: "Models",
title: "Models",
description: "Choose the model you want to use",
color: "bg-blue-500 hover:bg-blue-600",
},
{
id: "MCPs",
title: "MCPs",
description: "Choose the MCP you want to use",
color: "bg-purple-500 hover:bg-purple-600",
},
{
id: "Agents",
title: "Agents",
description: "Choose the agent you want to use",
color: "bg-emerald-500 hover:bg-emerald-600",
},
{
id: "Users",
title: "Users",
description: "Choose the user you want to use",
color: "bg-amber-500 hover:bg-amber-600",
},
];
interface SmoothTabProps {
items?: TabItem[];
defaultTabId?: string;
className?: string;
activeColor?: string;
onChange?: (tabId: string) => void;
}
const slideVariants = {
enter: (direction: number) => ({
x: direction > 0 ? "100%" : "-100%",
opacity: 0,
filter: "blur(8px)",
scale: 0.95,
position: "absolute" as const,
}),
center: {
x: 0,
opacity: 1,
filter: "blur(0px)",
scale: 1,
position: "absolute" as const,
},
exit: (direction: number) => ({
x: direction < 0 ? "100%" : "-100%",
opacity: 0,
filter: "blur(8px)",
scale: 0.95,
position: "absolute" as const,
}),
};
const transition = {
duration: 0.4,
ease: [0.32, 0.72, 0, 1],
};
export default function SmoothTab({
items = DEFAULT_TABS,
defaultTabId = DEFAULT_TABS[0].id,
className,
activeColor = "bg-[#1F9CFE]",
onChange,
}: SmoothTabProps) {
const [selected, setSelected] = React.useState(defaultTabId);
const [direction, setDirection] = React.useState(0);
const [dimensions, setDimensions] = React.useState({ width: 0, left: 0 });
// Reference for the selected button
const buttonRefs = React.useRef>(new Map());
const containerRef = React.useRef(null);
// Update dimensions whenever selected tab changes or on mount
React.useLayoutEffect(() => {
const updateDimensions = () => {
const selectedButton = buttonRefs.current.get(selected);
const container = containerRef.current;
if (selectedButton && container) {
const rect = selectedButton.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
setDimensions({
width: rect.width,
left: rect.left - containerRect.left,
});
}
};
// Initial update
requestAnimationFrame(() => {
updateDimensions();
});
// Update on resize
window.addEventListener("resize", updateDimensions);
return () => window.removeEventListener("resize", updateDimensions);
}, [selected]);
const handleTabClick = (tabId: string) => {
const currentIndex = items.findIndex((item) => item.id === selected);
const newIndex = items.findIndex((item) => item.id === tabId);
setDirection(newIndex > currentIndex ? 1 : -1);
setSelected(tabId);
onChange?.(tabId);
};
const handleKeyDown = (
e: React.KeyboardEvent,
tabId: string
) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleTabClick(tabId);
}
};
const selectedItem = items.find((item) => item.id === selected);
return (
{/* Card Content Area */}
{selectedItem?.cardContent ??
(selectedItem && (
))}
{/* Bottom Toolbar */}
{/* Sliding Background */}
{items.map((item) => {
const isSelected = selected === item.id;
return (
handleTabClick(item.id)}
onKeyDown={(e) => handleKeyDown(e, item.id)}
ref={(el) => {
if (el) buttonRefs.current.set(item.id, el);
else buttonRefs.current.delete(item.id);
}}
role="tab"
tabIndex={isSelected ? 0 : -1}
type="button"
>
{item.title}
);
})}
);
}
```
---
# Toolbar
> Figma-inspired toolbar where the selected tool expands to reveal its label with spring animations, built with React, Motion, and Tailwind CSS.
Source: https://kokonutui.com/docs/navigation/toolbar
## Installation
```bash
npx shadcn@latest add @kokonutui/toolbar
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Toolbar
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import {
Bell,
CircleUserRound,
Edit2,
FileDown,
Frame,
Layers,
Lock,
type LucideIcon,
MousePointer2,
Move,
Palette,
Shapes,
Share2,
SlidersHorizontal,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import * as React from "react";
import { cn } from "@/lib/utils";
interface ToolbarItem {
id: string;
title: string;
icon: LucideIcon;
type?: never;
}
interface ToolbarProps {
items?: ToolbarItem[];
defaultSelected?: string;
className?: string;
activeColor?: string;
onSelect?: (itemId: string) => void;
}
const DEFAULT_TOOLBAR_ITEMS: ToolbarItem[] = [
{ id: "select", title: "Select", icon: MousePointer2 },
{ id: "move", title: "Move", icon: Move },
{ id: "shapes", title: "Shapes", icon: Shapes },
{ id: "layers", title: "Layers", icon: Layers },
{ id: "frame", title: "Frame", icon: Frame },
{ id: "properties", title: "Properties", icon: SlidersHorizontal },
{ id: "export", title: "Export", icon: FileDown },
{ id: "share", title: "Share", icon: Share2 },
{ id: "notifications", title: "Notifications", icon: Bell },
{ id: "profile", title: "Profile", icon: CircleUserRound },
{ id: "appearance", title: "Appearance", icon: Palette },
];
const buttonVariants = {
initial: {
gap: 0,
paddingLeft: ".5rem",
paddingRight: ".5rem",
},
animate: (isSelected: boolean) => ({
gap: isSelected ? ".5rem" : 0,
paddingLeft: isSelected ? "1rem" : ".5rem",
paddingRight: isSelected ? "1rem" : ".5rem",
}),
};
const spanVariants = {
initial: { width: 0, opacity: 0 },
animate: { width: "auto", opacity: 1 },
exit: { width: 0, opacity: 0 },
};
const notificationVariants = {
initial: { opacity: 0, y: 10 },
animate: { opacity: 1, y: -10 },
exit: { opacity: 0, y: -20 },
};
const lineVariants = {
initial: { scaleX: 0, x: "-50%" },
animate: {
scaleX: 1,
x: "0%",
transition: { duration: 0.2, ease: "easeOut" },
},
exit: {
scaleX: 0,
x: "50%",
transition: { duration: 0.2, ease: "easeIn" },
},
};
const transition = { type: "spring", bounce: 0, duration: 0.4 };
export function Toolbar({
items = DEFAULT_TOOLBAR_ITEMS,
defaultSelected = "select",
className,
activeColor = "text-primary",
onSelect,
}: ToolbarProps) {
const [selected, setSelected] = React.useState(
defaultSelected
);
const [isToggled, setIsToggled] = React.useState(false);
const [activeNotification, setActiveNotification] = React.useState<
string | null
>(null);
const outsideClickRef = React.useRef(null);
const handleItemClick = (itemId: string) => {
setSelected(selected === itemId ? null : itemId);
onSelect?.(itemId);
setActiveNotification(itemId);
setTimeout(() => setActiveNotification(null), 1500);
};
return (
{activeNotification && (
{items.find((item) => item.id === activeNotification)?.title}{" "}
clicked!
)}
{items.map((item) => (
handleItemClick(item.id)}
transition={transition as any}
variants={buttonVariants as any}
>
{selected === item.id && (
{item.title}
)}
))}
setIsToggled(!isToggled)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{isToggled ? (
) : (
)}
{isToggled ? "On" : "Off"}
);
}
export default Toolbar;
```
---
# Dynamic Text
> Animated text switcher that cycles through greetings in multiple languages, built with React, Tailwind CSS and Motion enter and exit transitions.
Source: https://kokonutui.com/docs/texts/dynamic-text
## Installation
```bash
npx shadcn@latest add @kokonutui/dynamic-text
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Dynamic Text
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { AnimatePresence, motion } from "motion/react";
import { useEffect, useState } from "react";
interface Greeting {
text: string;
language: string;
}
const greetings: Greeting[] = [
{ text: "Hello", language: "English" },
{ text: "γγγ«γ‘γ―", language: "Japanese" },
{ text: "Bonjour", language: "French" },
{ text: "Hola", language: "Spanish" },
{ text: "μλ
νμΈμ", language: "Korean" },
{ text: "Ciao", language: "Italian" },
{ text: "Hallo", language: "German" },
{ text: "γγγ«γ‘γ―", language: "Japanese" },
];
const DynamicText = () => {
const [currentIndex, setCurrentIndex] = useState(0);
const [isAnimating, setIsAnimating] = useState(true);
useEffect(() => {
if (!isAnimating) return;
const interval = setInterval(() => {
setCurrentIndex((prevIndex) => {
const nextIndex = prevIndex + 1;
if (nextIndex >= greetings.length) {
clearInterval(interval);
setIsAnimating(false);
return prevIndex;
}
return nextIndex;
});
}, 300);
return () => clearInterval(interval);
}, [isAnimating]);
// Animation variants for the text
const textVariants = {
hidden: { y: 20, opacity: 0 },
visible: { y: 0, opacity: 1 },
exit: { y: -100, opacity: 0 },
};
return (
{isAnimating ? (
{greetings[currentIndex].text}
) : (
{greetings[currentIndex].text}
)}
);
};
export default DynamicText;
```
---
# Glitch Text
> Customizable glitch text effect with adjustable intensity, colors and sizes, animated in React with Motion and styled with Tailwind CSS.
Source: https://kokonutui.com/docs/texts/glitch-text
## Installation
```bash
npx shadcn@latest add @kokonutui/matrix-text
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Matrix Text
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { cn } from "@/lib/utils";
interface LetterState {
char: string;
isMatrix: boolean;
isSpace: boolean;
}
interface MatrixTextProps {
text?: string;
className?: string;
initialDelay?: number;
letterAnimationDuration?: number;
letterInterval?: number;
}
const MatrixText = ({
text = "HelloWorld!",
className,
initialDelay = 200,
letterAnimationDuration = 500,
letterInterval = 100,
}: MatrixTextProps) => {
const [letters, setLetters] = useState(() =>
text.split("").map((char) => ({
char,
isMatrix: false,
isSpace: char === " ",
}))
);
const [isAnimating, setIsAnimating] = useState(false);
const getRandomChar = useCallback(
() => (Math.random() > 0.5 ? "1" : "0"),
[]
);
const animateLetter = useCallback(
(index: number) => {
if (index >= text.length) return;
requestAnimationFrame(() => {
setLetters((prev) => {
const newLetters = [...prev];
if (!newLetters[index].isSpace) {
newLetters[index] = {
...newLetters[index],
char: getRandomChar(),
isMatrix: true,
};
}
return newLetters;
});
setTimeout(() => {
setLetters((prev) => {
const newLetters = [...prev];
newLetters[index] = {
...newLetters[index],
char: text[index],
isMatrix: false,
};
return newLetters;
});
}, letterAnimationDuration);
});
},
[getRandomChar, text, letterAnimationDuration]
);
const startAnimation = useCallback(() => {
if (isAnimating) return;
setIsAnimating(true);
let currentIndex = 0;
const animate = () => {
if (currentIndex >= text.length) {
setIsAnimating(false);
return;
}
animateLetter(currentIndex);
currentIndex++;
setTimeout(animate, letterInterval);
};
animate();
}, [animateLetter, text, isAnimating, letterInterval]);
useEffect(() => {
const timer = setTimeout(startAnimation, initialDelay);
return () => clearTimeout(timer);
}, []);
const motionVariants = useMemo(
() => ({
// initial: {
// color: "rgb(var(--foreground-rgb))",
// },
matrix: {
color: "#00ff00",
textShadow: "0 2px 4px rgba(0, 255, 0, 0.5)",
},
// normal: {
// color: "rgb(var(--foreground-rgb))",
// textShadow: "none",
// },
}),
[]
);
return (
{letters.map((letter, index) => (
{letter.isSpace ? "\u00A0" : letter.char}
))}
);
};
export default MatrixText;
```
---
# Matrix Text
> Matrix-style text effect where letters scramble through binary characters before revealing, built with React, Tailwind CSS and Motion animations.
Source: https://kokonutui.com/docs/texts/matrix-text
## Installation
```bash
npx shadcn@latest add @kokonutui/matrix-text
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Matrix Text
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { cn } from "@/lib/utils";
interface LetterState {
char: string;
isMatrix: boolean;
isSpace: boolean;
}
interface MatrixTextProps {
text?: string;
className?: string;
initialDelay?: number;
letterAnimationDuration?: number;
letterInterval?: number;
}
const MatrixText = ({
text = "HelloWorld!",
className,
initialDelay = 200,
letterAnimationDuration = 500,
letterInterval = 100,
}: MatrixTextProps) => {
const [letters, setLetters] = useState(() =>
text.split("").map((char) => ({
char,
isMatrix: false,
isSpace: char === " ",
}))
);
const [isAnimating, setIsAnimating] = useState(false);
const getRandomChar = useCallback(
() => (Math.random() > 0.5 ? "1" : "0"),
[]
);
const animateLetter = useCallback(
(index: number) => {
if (index >= text.length) return;
requestAnimationFrame(() => {
setLetters((prev) => {
const newLetters = [...prev];
if (!newLetters[index].isSpace) {
newLetters[index] = {
...newLetters[index],
char: getRandomChar(),
isMatrix: true,
};
}
return newLetters;
});
setTimeout(() => {
setLetters((prev) => {
const newLetters = [...prev];
newLetters[index] = {
...newLetters[index],
char: text[index],
isMatrix: false,
};
return newLetters;
});
}, letterAnimationDuration);
});
},
[getRandomChar, text, letterAnimationDuration]
);
const startAnimation = useCallback(() => {
if (isAnimating) return;
setIsAnimating(true);
let currentIndex = 0;
const animate = () => {
if (currentIndex >= text.length) {
setIsAnimating(false);
return;
}
animateLetter(currentIndex);
currentIndex++;
setTimeout(animate, letterInterval);
};
animate();
}, [animateLetter, text, isAnimating, letterInterval]);
useEffect(() => {
const timer = setTimeout(startAnimation, initialDelay);
return () => clearTimeout(timer);
}, []);
const motionVariants = useMemo(
() => ({
// initial: {
// color: "rgb(var(--foreground-rgb))",
// },
matrix: {
color: "#00ff00",
textShadow: "0 2px 4px rgba(0, 255, 0, 0.5)",
},
// normal: {
// color: "rgb(var(--foreground-rgb))",
// textShadow: "none",
// },
}),
[]
);
return (
{letters.map((letter, index) => (
{letter.isSpace ? "\u00A0" : letter.char}
))}
);
};
export default MatrixText;
```
---
# Scroll Text
> Scroll-driven text list that highlights the active word as you scroll, using IntersectionObserver with React, Tailwind CSS and Motion animations.
Source: https://kokonutui.com/docs/texts/scroll-text
## Installation
```bash
npx shadcn@latest add @kokonutui/scroll-text
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Scroll Text
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion, type Variants } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
interface ScrollTextProps {
texts?: string[];
className?: string;
}
export default function ScrollText({
texts = [
"TailwindCSS",
"Kokonut UI",
"shadcn/ui",
"Next.js",
"Vercel",
"Motion",
"React",
"Resend",
"TypeScript",
"Fumadocs",
"Supabase",
"Vercel",
],
className,
}: ScrollTextProps) {
const [activeIndex, setActiveIndex] = useState(0);
const observerRef = useRef(null);
const itemsRef = useRef<(HTMLDivElement | null)[]>([]);
const containerRef = useRef(null);
// Scroll to top on mount
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = 0;
}
}, []);
const handleIntersection = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const index = itemsRef.current.findIndex(
(item) => item === entry.target
);
setActiveIndex(index);
}
});
};
// Setup intersection observer
const setupObserver = (element: HTMLDivElement | null, index: number) => {
if (element && !itemsRef.current[index]) {
itemsRef.current[index] = element;
if (!observerRef.current) {
observerRef.current = new IntersectionObserver(handleIntersection, {
threshold: 0.7,
root: containerRef.current,
rootMargin: "-45% 0px -45% 0px",
});
}
observerRef.current.observe(element);
}
};
// Animation variants for the reveal effect
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
const itemVariants: Variants = {
hidden: (index: number) => ({
opacity: 0,
x: index % 2 === 0 ? -100 : 100,
rotate: index % 2 === 0 ? -10 : 10,
}),
visible: {
opacity: 1,
x: 0,
rotate: 0,
transition: {
type: "spring",
stiffness: 100,
damping: 15,
duration: 0.5,
},
},
};
return (
{texts.map((text, index) => (
setupObserver(el, index)}
variants={itemVariants}
viewport={{
once: false,
margin: "-20% 0px -20% 0px",
}}
whileInView="visible"
>
{text}
))}
);
}
```
---
# Shimmer Text
> Animated shimmer text with a looping gradient sweep across the letters, built with React, Tailwind CSS gradients and Motion animations.
Source: https://kokonutui.com/docs/texts/shimmer-text
## Installation
```bash
npx shadcn@latest add @kokonutui/shimmer-text
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Shimmer Text
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface Text_01Props {
text: string;
className?: string;
}
export default function ShimmerText({
text = "Text Shimmer",
className,
}: Text_01Props) {
return (
{text}
);
}
```
---
# Sliced Text
> Text effect that splits into two offset slices and merges together on hover using clip-path, built with React, Tailwind CSS and Motion animations.
Source: https://kokonutui.com/docs/texts/sliced-text
## Installation
```bash
npx shadcn@latest add @kokonutui/sliced-text
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Sliced Text
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface SlicedTextProps {
text: string;
className?: string;
containerClassName?: string;
splitSpacing?: number;
}
const SlicedText: React.FC = ({
text = "Sliced Text",
className = "",
containerClassName = "",
splitSpacing = 2,
}) => (
{text}
{text}
{text}
);
export default SlicedText;
```
---
# Swoosh Text
> Hover text effect with layered colorful shadows that snap flat on mouseover, built with React, Tailwind CSS and Motion hover animations.
Source: https://kokonutui.com/docs/texts/swoosh-text
## Installation
```bash
npx shadcn@latest add @kokonutui/swoosh-text
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Swoosh Text
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { cn } from "@/lib/utils";
interface SwooshTextProps {
text?: string;
className?: string;
shadowColors?: {
first?: string;
second?: string;
third?: string;
fourth?: string;
glow?: string;
};
}
export default function SwooshText({
text = "Hover Me",
className = "",
shadowColors = {
first: "#07bccc",
second: "#e601c0",
third: "#e9019a",
fourth: "#f40468",
glow: "#f40468",
},
}: SwooshTextProps) {
const textShadowStyle = {
textShadow: `10px 10px 0px ${shadowColors.first},
15px 15px 0px ${shadowColors.second},
20px 20px 0px ${shadowColors.third},
25px 25px 0px ${shadowColors.fourth},
45px 45px 10px ${shadowColors.glow}`,
};
const noShadowStyle = {
textShadow: "none",
};
return (
{text}
);
}
```
---
# Typing Text
> Typewriter text animation that types and deletes multiple sequences with a blinking cursor and auto loop, built with React and Motion.
Source: https://kokonutui.com/docs/texts/type-writer
## Installation
```bash
npx shadcn@latest add @kokonutui/type-writer
```
## Source
```tsx
"use client";
/**
* @author: @dorianbaffier
* @description: Typewriter
* @version: 1.0.0
* @date: 2025-06-26
* @license: MIT
* @website: https://kokonutui.com
* @github: https://github.com/kokonut-labs/kokonutui
*/
import { motion } from "motion/react";
import { useEffect, useRef, useState } from "react";
type TypewriterSequence = {
text: string;
deleteAfter?: boolean;
pauseAfter?: number;
};
type TypewriterTitleProps = {
sequences?: TypewriterSequence[];
typingSpeed?: number;
startDelay?: number;
autoLoop?: boolean;
loopDelay?: number;
deleteSpeed?: number;
pauseBeforeDelete?: number;
naturalVariance?: boolean;
};
const DEFAULT_SEQUENCES: TypewriterSequence[] = [
{ text: "Typewriter", deleteAfter: true },
{ text: "Multiple Words", deleteAfter: true },
{ text: "Auto Loop", deleteAfter: false },
];
export default function TypewriterTitle({
sequences = DEFAULT_SEQUENCES,
typingSpeed = 50,
startDelay = 200,
autoLoop = true,
loopDelay = 1000,
deleteSpeed = 30,
pauseBeforeDelete = 1000,
naturalVariance = true,
}: TypewriterTitleProps) {
const [displayText, setDisplayText] = useState("");
const sequenceIndexRef = useRef(0);
const charIndexRef = useRef(0);
const isDeletingRef = useRef(false);
const timeoutRef = useRef(null);
// Initialize with the sequences provided
const sequencesRef = useRef(sequences);
useEffect(() => {
sequencesRef.current = sequences;
}, [sequences]);
useEffect(() => {
const getTypingDelay = () => {
if (!naturalVariance) {
return typingSpeed;
}
// More natural human typing pattern
const random = Math.random();
// 10% chance of a longer pause (thinking/hesitation)
if (random < 0.1) {
return typingSpeed * 2;
}
// 10% chance of a burst (fast typing)
if (random > 0.9) {
return typingSpeed * 0.5;
}
// Standard variance (+/- 40%)
const variance = 0.4;
const min = typingSpeed * (1 - variance);
const max = typingSpeed * (1 + variance);
return Math.random() * (max - min) + min;
};
const runTypewriter = () => {
const currentSequence = sequencesRef.current[sequenceIndexRef.current];
if (!currentSequence) {
return;
}
if (isDeletingRef.current) {
if (charIndexRef.current > 0) {
charIndexRef.current -= 1;
setDisplayText(currentSequence.text.slice(0, charIndexRef.current));
timeoutRef.current = setTimeout(runTypewriter, deleteSpeed);
} else {
isDeletingRef.current = false;
const isLastSequence =
sequenceIndexRef.current === sequencesRef.current.length - 1;
if (isLastSequence && autoLoop) {
timeoutRef.current = setTimeout(() => {
sequenceIndexRef.current = 0;
runTypewriter();
}, loopDelay);
} else if (!isLastSequence) {
timeoutRef.current = setTimeout(() => {
sequenceIndexRef.current += 1;
runTypewriter();
}, 100); // Quick transition to next word
}
}
} else if (charIndexRef.current < currentSequence.text.length) {
charIndexRef.current += 1;
setDisplayText(currentSequence.text.slice(0, charIndexRef.current));
timeoutRef.current = setTimeout(runTypewriter, getTypingDelay());
} else {
const pauseDuration = currentSequence.pauseAfter ?? pauseBeforeDelete;
if (currentSequence.deleteAfter) {
timeoutRef.current = setTimeout(() => {
isDeletingRef.current = true;
runTypewriter();
}, pauseDuration);
} else {
const isLastSequence =
sequenceIndexRef.current === sequencesRef.current.length - 1;
if (isLastSequence && autoLoop) {
timeoutRef.current = setTimeout(() => {
sequenceIndexRef.current = 0;
charIndexRef.current = 0;
setDisplayText("");
runTypewriter();
}, loopDelay);
} else if (!isLastSequence) {
timeoutRef.current = setTimeout(() => {
sequenceIndexRef.current += 1;
charIndexRef.current = 0;
setDisplayText("");
runTypewriter();
}, pauseDuration);
}
}
}
};
// Start the loop
timeoutRef.current = setTimeout(runTypewriter, startDelay);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, [
// Only restart effect if timing configs change.
// We use sequencesRef for content to avoid restarting on array reference change.
typingSpeed,
deleteSpeed,
pauseBeforeDelete,
autoLoop,
loopDelay,
startDelay,
naturalVariance,
]);
return (
);
}
```