# Introduction (/docs/introduction) --- title: Introduction description: A set of perfect-designed components built on top of React Aria and Motion. --- **Kanpeki** is a collection of beautifully designed, accessible, and composable components for your React apps. Built on top of [React Aria Components](https://react-aria.adobe.com/) and styled with [Tailwind CSS](https://tailwindcss.com/), it's designed for you to copy, paste, and own. We think React Aria Components is the best foundation for modern web applications. We've taken its powerful, accessible primitives and given them a design system that's ready to go, right out of the box. ## How It Works Our approach is simple: you should own your code. We're inspired by the copy-paste ethos of **shadcn/ui**. Instead of installing a package, you get the source code. This means: - **No abstractions:** You have full control over how components look and behave. - **Endless customization:** Need to change something? Just edit the file. - **Learn by doing:** See how components are built and adapt them to your needs. **What do you mean by not a component library?** You do not install it as a dependency. It is not available or distributed via npm. Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours. ## Like shadcn/ui, but with React Aria If you've used shadcn/ui, you'll feel right at home. The main difference is the foundation: **shadcn/ui uses Radix UI**, while **Kanpeki uses React Aria Components**. We chose React Aria Components for its robust, accessible, and unopinionated primitives. It handles the hard parts—like accessibility, keyboard navigation, and focus management—without imposing any styles. This gives us the freedom to create a design system that is both beautiful and flexible. ## Modern Tooling Kanpeki uses modern tools that improve developer experience: ### CVA Beta (Class Variance Authority) Instead of the old `class-variance-authority` package, we use the new beta version simply called [`cva`](https://beta.cva.style/). This new version addresses one of the biggest complaints: the name was just too long. **Key improvements:** - **Simpler configuration:** No more `cn` utility hack. Configure `tailwind-merge` directly: ```ts title="lib/cva.ts" import { defineConfig } from "cva"; import { twMerge } from "tailwind-merge"; export const { cva, cx: cn, compose, } = defineConfig({ hooks: { onComplete: (className) => twMerge(className), }, }); ``` Compare this to the old shadcn approach: ```ts title="lib/utils.ts" import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ``` - **New features:** - [`compose`](https://beta.cva.style/getting-started/composing-components) - Shallow merge multiple `cva` components - [`defineConfig`](https://beta.cva.style/api-reference#defineconfig) - Generate `cva`, `cx` and `compose` functions based on your preferred configuration - [Cleaner API with single parameter (base styles via `base` property)](https://beta.cva.style/getting-started/whats-new#2-cva-now-accepts-a-single-parameter) - [Better TypeScript support](https://beta.cva.style/getting-started/whats-new#4-clearer-type-guards) This is why Kanpeki doesn't need `class-variance-authority` or `clsx`. ### Tailwind CSS Motion Instead of `tw-animate-css`, we use [`tailwindcss-motion`](https://docs.rombo.co/tailwind) for animations. It provides: - More animation presets out of the box - Better customization options - More flexible animation controls - Modern animation utilities ## Open Source This project is open source and built in public. We welcome contributions, feedback, and improvements. ## Inspirations Here are some cool projects that inspired Kanpeki: - [shadcn/ui](https://ui.shadcn.com) - [coss ui](https://coss.com/ui/docs) - [Intent UI](https://intentui.com) - [Radix Themes](https://www.radix-ui.com) - [dotUI](https://dotui.org) - [JollyUI](https://www.jollyui.dev) - [Magic UI](https://magicui.design) - [Aceternity UI](https://ui.aceternity.com) - [Luxe](https://www.luxeui.com) # Typography (/docs/typography) --- title: Typography description: Styles for headings, paragraphs, lists...etc --- ## Component Preview: typography-demo ```tsx export function TypographyDemo() { return (

The Joke Tax Chronicles

Once upon a time, in a far-off land, there was a very lazy king who spent all day lounging on his throne. One day, his advisors came to him with a problem: the kingdom was running out of money.

The King's Plan

The king thought long and hard, and finally came up with{" "} a brilliant plan : he would tax the jokes in the kingdom.

"After all," he said, "everyone enjoys a good joke, so it's only fair that they should pay for the privilege."

The Joke Tax

The king's subjects were not amused. They grumbled and complained, but the king was firm:

As a result, people stopped telling jokes, and the kingdom fell into a gloom. But there was one person who refused to let the king's foolishness get him down: a court jester named Jokester.

Jokester's Revolt

Jokester began sneaking into the castle in the middle of the night and leaving jokes all over the place: under the king's pillow, in his soup, even in the royal toilet. The king was furious, but he couldn't seem to stop Jokester.

And then, one day, the people of the kingdom discovered that the jokes left by Jokester were so funny that they couldn't help but laugh. And once they started laughing, they couldn't stop.

The People's Rebellion

The people of the kingdom, feeling uplifted by the laughter, started to tell jokes and puns again, and soon the entire kingdom was in on the joke.

King's Treasury People's happiness
Empty Overflowing
Modest Satisfied
Full Ecstatic

The king, seeing how much happier his subjects were, realized the error of his ways and repealed the joke tax. Jokester was declared a hero, and the kingdom lived happily ever after.

The moral of the story is: never underestimate the power of a good laugh and always be careful of bad ideas.

); } ``` ## Installation ```css title="typography.css" @utility heading-1 { @apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl; } @utility heading-2 { @apply scroll-m-20 border-b pb-2 text-3xl font-semibold tracking-tight first:mt-0; } @utility heading-3 { @apply scroll-m-20 text-2xl font-semibold tracking-tight; } @utility heading-4 { @apply scroll-m-20 text-xl font-semibold tracking-tight; } @utility p { @apply leading-7 not-first:mt-6; } @utility blockquote { @apply mt-6 border-l-2 pl-6 italic; } ``` ```css title="globals.css" @import "tailwindcss"; @import "./typography.css"; /* [!code highlight] */ ``` ## Examples ### h1 ## Component Preview: typography-h1-demo ```tsx export function TypographyH1Demo() { return (

Taxing Laughter: The Joke Tax Chronicles

); } ``` ### h2 ## Component Preview: typography-h2-demo ```tsx export function TypographyH2Demo() { return

The People of the Kingdom

; } ``` ### h3 ## Component Preview: typography-h3-demo ```tsx export function TypographyH3Demo() { return

The Joke Tax

; } ``` ### h4 ## Component Preview: typography-h4-demo ```tsx export function TypographyH4Demo() { return

People stopped telling jokes

; } ``` ### p ## Component Preview: typography-p-demo ```tsx export function TypographyPDemo() { return (

The king, seeing how much happier his subjects were, realized the error of his ways and repealed the joke tax.

); } ``` ### blockquote ## Component Preview: typography-blockquote-demo ```tsx export function TypographyBlockquoteDemo() { return (
"After all," he said, "everyone enjoys a good joke, so it's only fair that they should pay for the privilege."
); } ``` # Accordion (/docs/components/accordion) --- title: Accordion description: A vertically stacked set of interactive headings that each reveal a section of content. links: docs: https://react-aria.adobe.com/DisclosureGroup --- ## Component Preview: accordion-demo ```tsx import { Accordion } from "~/components/ui/accordion"; export function AccordionDemo() { return ( Is it accessible? Yes. It adheres to the WAI-ARIA design pattern. Is it styled? Yes. It comes with default styles that matches the other components' aesthetic. Is it animated? Yes. It's animated by default, but you can disable it if you prefer. ); } ``` ## Installation CLI Manual ```bash npx shadcn-ui@latest add @kanpeki/accordion ``` Copy and paste the following code into your project. ## Component Source: accordion ```tsx // File: index.ts export * from "./accordion"; export * as Accordion from "./namespace"; // File: accordion.tsx "use client"; import { ChevronDownIcon } from "lucide-react"; import { Button, composeRenderProps, Disclosure, DisclosureGroup, DisclosurePanel, } from "react-aria-components"; import { AccordionStyles } from "./styles"; export interface AccordionRootProps extends React.ComponentProps {} export function AccordionRoot(props: AccordionRootProps) { return ; } export interface AccordionItemProps extends React.ComponentProps {} export function AccordionItem({ className, ...props }: AccordionItemProps) { return ( AccordionStyles.Item({ className }) )} data-slot="accordion-item" {...props} /> ); } export interface AccordionTriggerProps extends Omit, "slot"> {} export function AccordionTrigger({ className, children, ...props }: AccordionTriggerProps) { return ( ); } export interface AccordionContentProps extends React.ComponentProps {} export function AccordionContent({ className, children, ...props }: AccordionContentProps) { return ( AccordionStyles.Content({ className }) )} data-slot="accordion-content" {...props} >
{children}
); } // File: styles.ts import { compose, cva } from "~/lib/cva"; import { CollapsibleStyles } from "~/components/ui/collapsible"; export const AccordionStyles = { Content: compose( CollapsibleStyles.Content, cva({ base: ["*:pb-4"], }) ), Icon: cva({ base: [ "pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200 group-expanded:rotate-180", ], }), Item: cva({ base: ["group border-b last:border-b-0"], }), Trigger: cva({ base: [ "flex w-full flex-1 items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all", "hover:underline", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "disabled:pointer-events-none disabled:opacity-50", ], }), }; // File: namespace.ts export { AccordionContent as Content, AccordionItem as Item, AccordionRoot as Root, AccordionTrigger as Trigger, } from "./accordion"; ``` Update the import paths to match your project setup.
## Anatomy Single import Multiple imports ```tsx import { Accordion } from "~/components/ui/accordion"; Is it accessible? Yes. It adheres to the WAI-ARIA design pattern. ; ``` ```tsx import { AccordionContent, AccordionItem, AccordionRoot, AccordionTrigger, } from "~/components/ui/accordion"; Is it accessible? Yes. It adheres to the WAI-ARIA design pattern. ; ``` ## Animation The accordion uses the `--disclosure-panel-height{:css}` CSS variable to animate the height of the content area. ## Customization If you need to customize the internal padding of the accordion content, you can use the `*:` Tailwind variant to target the content area directly. ```tsx Customized padding for the content area. ``` # Alert (/docs/components/alert) --- title: Alert description: Displays a callout for user attention. links: docs: https://ui.shadcn.com/docs/components/alert --- ## Component Preview: alert-demo ```tsx import { CheckCircle2Icon } from "lucide-react"; import { Alert } from "~/components/ui/alert"; export function AlertDemo() { return ( Success! Your changes have been saved This is an alert. with icon, title and description. ); } ``` ## Installation CLI Manual ```bash npx shadcn@latest add @kanpeki/alert ``` Copy and paste the following code into your project. ## Component Source: alert ```tsx // File: index.ts export * from "./alert"; export * as Alert from "./namespace"; export * from "./styles"; // File: alert.tsx import type { VariantProps } from "cva"; import { AlertStyles } from "./styles"; export interface AlertRootProps extends React.ComponentProps<"div">, VariantProps {} export function AlertRoot({ className, variant, ...props }: AlertRootProps) { return (
); } export interface AlertTitleProps extends React.ComponentProps<"div"> {} export function AlertTitle({ className, ...props }: AlertTitleProps) { return (
); } export interface AlertDescriptionProps extends React.ComponentProps<"div"> {} export function AlertDescription({ className, ...props }: AlertDescriptionProps) { return (
); } // File: styles.ts import { cva } from "~/lib/cva"; export const AlertStyles = { Description: cva({ base: [ "col-start-2 grid justify-items-start gap-1 text-muted-foreground text-sm [&_p]:leading-relaxed", ], }), Root: cva({ base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", defaultVariants: { variant: "default", }, variants: { variant: { default: "bg-card text-card-foreground", destructive: "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current", warning: "bg-card text-amber-400 *:data-[slot=alert-description]:text-amber-400/90 [&>svg]:text-current", }, }, }), Title: cva({ base: ["col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight"], }), }; // File: namespace.ts export { AlertDescription as Description, AlertRoot as Root, AlertTitle as Title, } from "./alert"; ``` Update the import paths to match your project setup. ## Anatomy Single import Multiple imports ```tsx import { TerminalIcon } from "lucide-react"; import { Alert } from "~/components/ui/alert"; Heads up! You can add components and dependencies to your app using the cli. ``` ```tsx import { TerminalIcon } from "lucide-react"; import { AlertRoot, AlertDescription, AlertTitle } from "~/components/ui/alert"; Heads up! You can add components and dependencies to your app using the cli. ``` ## Examples ### Destructive ## Component Preview: alert-destructive-demo ```tsx import { AlertCircleIcon } from "lucide-react"; import { Alert } from "~/components/ui/alert"; export function AlertDestructiveDemo() { return ( Something went wrong! Your session has expired. Please log in again. ); } ``` ## API Reference # Autocomplete (/docs/components/autocomplete) --- title: Autocomplete description: An autocomplete combines a TextField or SearchField with a Menu or ListBox, allowing users to search or filter a list of suggestions. links: docs: https://react-aria.adobe.com/Autocomplete --- ## Component Preview: autocomplete-demo ```tsx "use client"; import { CalculatorIcon, CalendarIcon, CreditCardIcon, SearchIcon, SettingsIcon, SmileIcon, UserIcon, } from "lucide-react"; import { Autocomplete } from "~/components/ui/autocomplete"; import { Card } from "~/components/ui/card"; import { InputGroup } from "~/components/ui/input-group"; import { Keyboard } from "~/components/ui/keyboard"; import { Menu } from "~/components/ui/menu"; import { SearchField } from "~/components/ui/search-field"; export function AutocompleteDemo() { return ( } > No results found.} > Suggestions Calendar Search Emoji Calculator Settings Profile ⌘P Billing ⌘B Settings ⌘S ); } ``` ## Installation CLI Manual ```bash npx shadcn@latest add @kanpeki/autocomplete ``` Copy and paste the following code into your project. ## Component Source: autocomplete ```tsx "use client"; import { Autocomplete as RACAutocomplete, useFilter, } from "react-aria-components"; export interface AutocompleteProps extends React.ComponentProps { options?: Intl.CollatorOptions; } export function Autocomplete({ filter, options, ...props }: AutocompleteProps) { const { contains } = useFilter({ sensitivity: "base", ...options }); return ; } ``` Update the import paths to match your project setup. ## Anatomy ```tsx import { Autocomplete } from "~/components/ui/autocomplete"; import { Menu } from "~/components/ui/menu"; import { SearchField } from "~/components/ui/search-field"; or or ; ``` ## Examples ### Dialog ## Component Preview: autocomplete-dialog-demo ```tsx "use client"; import { CalculatorIcon, CalendarIcon, CreditCardIcon, SearchIcon, SettingsIcon, SmileIcon, UserIcon, } from "lucide-react"; import { useEffect, useState } from "react"; import { Autocomplete } from "~/components/ui/autocomplete"; import { Dialog } from "~/components/ui/dialog"; import { InputGroup } from "~/components/ui/input-group"; import { Keyboard } from "~/components/ui/keyboard"; import { Menu } from "~/components/ui/menu"; import { SearchField } from "~/components/ui/search-field"; export function AutocompleteDialogDemo() { const [isOpen, setIsOpen] = useState(false); useEffect(() => { const controller = new AbortController(); document.addEventListener( "keydown", (e) => { if (e.key === "j" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); setIsOpen((isOpen) => !isOpen); } }, { signal: controller.signal, } ); return () => controller.abort(); }, []); return (

Press{" "} J

} > ( No results found. )} variant="command" > Suggestions Calendar Search Emoji Calculator Settings Profile ⌘P Billing ⌘B Settings ⌘S
); } ``` # Avatar (/docs/components/avatar) --- title: Avatar description: An image element with a fallback for representing the user. --- ## Component Preview: avatar-demo ```tsx import { UserIcon } from "lucide-react"; import { Avatar } from "~/components/ui/avatar"; export function AvatarDemo() { return ( FU ); } ``` ## Installation CLI Manual ```bash npx shadcn@latest add @kanpeki/avatar ``` Copy and paste the following code into your project. ## Component Source: avatar ```tsx // File: index.ts export * from "./avatar"; export * as Avatar from "./namespace"; export * from "./styles"; // File: avatar.tsx "use client"; import { createContext, use, useLayoutEffect, useState } from "react"; import { AvatarStyles } from "./styles"; type Status = "idle" | "loading" | "success" | "error"; const AvatarContext = createContext<{ status: Status; onStatusChange: (status: Status) => void; } | null>(null); function useAvatarContext() { const context = use(AvatarContext); if (!context) { throw new Error("Avatar components must be rendered within the AvatarRoot"); } return context; } export interface AvatarRootProps extends React.ComponentProps<"span"> {} export function AvatarRoot({ className, ...props }: AvatarRootProps) { const [status, setStatus] = useState("idle"); return ( ); } function useImageLoadingStatus(src?: string) { const [status, setStatus] = useState("idle"); useLayoutEffect(() => { if (!src) { setStatus("error"); return; } let isMounted = true; const image = new window.Image(); const updateStatus = (status: Status) => () => { if (!isMounted) { return; } setStatus(status); }; setStatus("loading"); image.onload = updateStatus("success"); image.onerror = updateStatus("error"); image.src = src; return () => { isMounted = false; }; }, [src]); return status; } export interface AvatarImageProps extends React.ComponentProps<"img"> { src?: string; } export function AvatarImage({ className, src, width, height, ...props }: AvatarImageProps) { const context = useAvatarContext(); const status = useImageLoadingStatus(src); useLayoutEffect(() => { if (status !== "idle") { context.onStatusChange(status); } }, [status, context.onStatusChange]); if (status !== "success") { return null; } return ( {props.alt} ); } export interface AvatarFallbackProps extends React.ComponentProps<"span"> {} export function AvatarFallback({ className, ...props }: AvatarFallbackProps) { const { status } = useAvatarContext(); return ( ); } export interface AvatarPlaceholderProps extends React.ComponentProps<"span"> {} export function AvatarPlaceholder({ className, ...props }: AvatarPlaceholderProps) { const { status } = useAvatarContext(); if (status !== "idle" && status !== "loading") { return null; } return ( ); } // File: styles.ts import { cva } from "~/lib/cva"; export const AvatarStyles = { Fallback: cva({ base: [ "col-start-1 row-start-1 size-full select-none place-content-center rounded-full bg-muted", "grid group-data-[status=success]:hidden", ], }), Image: cva({ base: ["aspect-square size-full"], }), Placeholder: cva({ base: [ "col-start-1 row-start-1 grid size-full animate-pulse place-content-center bg-muted", ], }), Root: cva({ base: ["group relative grid size-8 shrink-0 overflow-hidden rounded-full"], }), }; // File: namespace.ts export { AvatarFallback as Fallback, AvatarImage as Image, AvatarPlaceholder as Placeholder, AvatarRoot as Root, } from "./avatar"; ``` Update the import paths to match your project setup. ## Anatomy Single import Multiple imports ```tsx import { UserIcon } from "lucide-react"; import { Avatar } from "~/components/ui/avatar"; FU ``` ```tsx import { UserIcon } from "lucide-react"; import { AvatarRoot, AvatarFallback, AvatarImage, AvatarPlaceholder, } from "~/components/ui/avatar"; FU ``` ## Examples ### Group ## Component Preview: avatar-group-demo ```tsx import { UserIcon } from "lucide-react"; import { Avatar } from "~/components/ui/avatar"; const avatars = [ { src: "https://github.com/fellipeutaka.png", alt: "@fellipeutaka", fallback: "FU", }, { src: "https://github.com/shadcn.png", alt: "@shadcn", fallback: "SN", }, { src: "https://github.com/devongovett.png", alt: "@devongovett", fallback: "DG", }, { src: "https://github.com/benoitgrelard.png", alt: "@benoitgrelard", fallback: "BG", }, { src: "https://github.com/adamwathan.png", alt: "@adamwathan", fallback: "AW", }, ] as const satisfies { src: string; alt: string; fallback: string; }[]; export function AvatarGroupDemo() { return (
{avatars.map((avatar) => ( {avatar.fallback} ))} +3
); } ``` ## API Reference ### Root Contains all the parts of an avatar. ### Image The image to render. By default it will only render when it has loaded. ### Fallback An element that renders when the image hasn't loaded. This means whilst it's loading, or if there was an error. ### Placeholder An element that renders when the image is loading. # Badge (/docs/components/badge) --- title: Badge description: Displays a badge or a component that looks like a badge. --- ## Component Preview: badge-demo ```tsx import { Badge } from "~/components/ui/badge"; export function BadgeDemo() { return Badge; } ``` ## Installation CLI Manual ```bash npx shadcn@latest add @kanpeki/badge ``` Copy and paste the following code into your project. ## Component Source: badge ```tsx // File: index.ts export * from "./badge"; export * from "./styles"; // File: badge.tsx import type { VariantProps } from "cva"; import { BadgeStyles } from "./styles"; export interface BadgeProps extends React.ComponentProps<"span">, VariantProps {} export function Badge({ className, variant, ...props }: BadgeProps) { return ( ); } // File: styles.ts import { cva } from "~/lib/cva"; export const BadgeStyles = cva({ base: "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border px-2 py-0.5 font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-3", variants: { variant: { default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", secondary: "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", destructive: "border-transparent bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", }, }, defaultVariants: { variant: "default", }, }); ``` Update the import paths to match your project setup. ## Anatomy ```tsx import { Badge } from "~/components/ui/badge"; Badge ``` ## Link You can use the `BadgeStyles` helper to create a link that looks like a badge. ```tsx import { BadgeStyles } from "~/components/ui/badge"; Badge ``` ## Examples ### Secondary ## Component Preview: badge-secondary-demo ```tsx import { Badge } from "~/components/ui/badge"; export function BadgeSecondaryDemo() { return Badge; } ``` ### Outline ## Component Preview: badge-outline-demo ```tsx import { Badge } from "~/components/ui/badge"; export function BadgeOutlineDemo() { return Badge; } ``` ### Destructive ## Component Preview: badge-destructive-demo ```tsx import { Badge } from "~/components/ui/badge"; export function BadgeDestructiveDemo() { return Badge; } ``` ## API Reference # Breadcrumb (/docs/components/breadcrumb) --- title: Breadcrumb description: Breadcrumbs display a hierarchy of links to the current page or resource in an application. links: docs: https://react-aria.adobe.com/Breadcrumbs --- ## Component Preview: breadcrumb-demo ```tsx import { Breadcrumb } from "~/components/ui/breadcrumb"; import { Button } from "~/components/ui/button/button"; import { Link } from "~/components/ui/link/link"; import { Menu } from "~/components/ui/menu"; import { Popover } from "~/components/ui/popover"; export function BreadcrumbDemo() { return ( Home Documentation Themes GitHub Components Breadcrumb ); } ``` ## Installation CLI Manual ```bash npx shadcn@latest add @kanpeki/breadcrumb ``` Copy and paste the following code into your project. ## Component Source: breadcrumb ```tsx // File: index.ts export * from "./breadcrumb"; export * as Breadcrumb from "./namespace"; export * from "./styles"; // File: breadcrumb.tsx "use client"; import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"; import { Breadcrumb, Breadcrumbs, composeRenderProps, } from "react-aria-components"; import { BreadcrumbStyles } from "./styles"; export interface BreadcrumbRootProps extends React.ComponentProps> {} export function BreadcrumbRoot({ className, ...props }: BreadcrumbRootProps) { return ( ); } export interface BreadcrumbItemProps extends React.ComponentProps {} export function BreadcrumbItem({ className, ...props }: BreadcrumbItemProps) { return ( ); } export interface BreadcrumbSeparatorProps extends React.ComponentProps {} export function BreadcrumbSeparator({ children, className, ...props }: BreadcrumbSeparatorProps) { return ( BreadcrumbStyles.Separator({ className }) )} data-slot="breadcrumb-separator" {...props} > {children ?? } ); } export interface BreadcrumbEllipsisProps extends React.ComponentProps<"span"> {} export function BreadcrumbEllipsis({ className, ...props }: BreadcrumbEllipsisProps) { return ( ); } // File: styles.ts import { cva } from "~/lib/cva"; export const BreadcrumbStyles = { Ellipsis: cva({ base: ["flex size-9 items-center justify-center"], }), Item: cva({ base: ["inline-flex items-center gap-1.5"], }), Root: cva({ base: [ "flex flex-wrap items-center gap-1.5 break-words text-muted-foreground text-sm sm:gap-2.5", ], }), Separator: cva({ base: ["[&>svg]:size-3.5"], }), }; // File: namespace.ts export { BreadcrumbEllipsis as Ellipsis, BreadcrumbItem as Item, BreadcrumbRoot as Root, BreadcrumbSeparator as Separator, } from "./breadcrumb"; ``` Update the import paths to match your project setup. ## Anatomy Single import Multiple imports ```tsx import { Breadcrumb } from "~/components/ui/breadcrumb"; import { Link } from "~/components/ui/link"; ; ``` ```tsx import { BreadcrumbRoot, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbEllipsis, BreadcrumbPage, } from "~/components/ui/breadcrumb"; import { Link } from "~/components/ui/link"; ; ``` ## Examples ### Custom Separator Use a custom component as `children` for `{:tsx}` to create a custom separator. ## Component Preview: breadcrumb-custom-separator-demo ```tsx import { SlashIcon } from "lucide-react"; import { Breadcrumb } from "~/components/ui/breadcrumb"; import { Link } from "~/components/ui/link"; export function BreadcrumbCustomSeparatorDemo() { return ( Home Components Breadcrumb ); } ``` ```tsx import { Breadcrumb } from "~/components/ui/breadcrumb"; import { SlashIcon } from "lucide-react"; // [!code highlight] {/* ... */} {/* [!code highlight:3] */} {/* ... */} ; ``` ### Dropdown You can compose `{:tsx}` with a `{:tsx}` to create a dropdown in the breadcrumb. ## Component Preview: breadcrumb-dropdown-demo ```tsx import { Breadcrumb } from "~/components/ui/breadcrumb"; import { RACButton } from "~/components/ui/button"; import { Link } from "~/components/ui/link"; import { LinkStyles } from "~/components/ui/link/styles"; import { Menu } from "~/components/ui/menu"; import { Popover } from "~/components/ui/popover"; export function BreadcrumbDropdownDemo() { return ( Docs Components Documentation Themes GitHub Breadcrumb ); } ``` ```tsx import { Button } from "react-aria-components"; // [!code highlight] import { Breadcrumb } from "~/components/ui/breadcrumb"; // [!code highlight] import { Menu } from "~/components/ui/menu"; // [!code highlight] import { LinkStyles } from "~/components/ui/link"; // [!code highlight] import { Popover } from "~/components/ui/popover"; // [!code highlight] {/* ... */} Documentation Themes GitHub {/* ... */} ; ``` ### Collapsed There's a `{:tsx}` component to show a collapsed state when the breadcrumb is too long. ## Component Preview: breadcrumb-collapsed-demo ```tsx import { Breadcrumb } from "~/components/ui/breadcrumb"; import { Link } from "~/components/ui/link"; export function BreadcrumbCollapsedDemo() { return ( Home Components Breadcrumb ); } ``` ```tsx import { Breadcrumb } from "~/components/ui/breadcrumb"; // [!code highlight] {/* ... */} {/* [!code highlight] */} {/* ... */} ; ``` # Button (/docs/components/button) --- title: Button description: Displays a button or a component that looks like a button. links: docs: https://react-aria.adobe.com/Button --- ## Component Preview: button-demo ```tsx import { Button } from "~/components/ui/button"; export function ButtonDemo() { return ; } ``` ## Installation CLI Manual ```bash npx shadcn@latest add @kanpeki/button ``` Copy and paste the following code into your project. ## Component Source: button ```tsx // File: index.ts export * from "./button"; export * from "./styles"; // File: button.tsx "use client"; import type { VariantProps } from "cva"; import { composeRenderProps, Button as RACButton } from "react-aria-components"; import { ButtonStyles } from "./styles"; export interface ButtonProps extends React.ComponentProps, VariantProps {} export function Button({ className, variant, size, ...props }: ButtonProps) { return ( ButtonStyles({ className, size, variant }) )} data-slot="button" {...props} /> ); } export { RACButton }; // File: styles.ts import { cva } from "~/lib/cva"; export const ButtonStyles = cva({ base: [ "inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md font-medium text-sm outline-none transition-all", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "disabled:cursor-not-allowed disabled:opacity-50", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", "[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", "pressed:scale-95", ], variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", unstyled: null, }, size: { default: "h-9 px-4 py-2 has-[>svg]:px-3", sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", lg: "h-10 rounded-md px-6 has-[>svg]:px-4", icon: "size-9", "icon-sm": "size-8", "icon-lg": "size-10", }, }, defaultVariants: { variant: "default", size: "default", }, }); ``` Update the import paths to match your project setup. ## Anatomy ```tsx import { Button } from "~/components/ui/button"; ; ``` ## Examples ### Sizes ## Component Preview: button-sizes-demo ```tsx import { ArrowUpRightIcon } from "lucide-react"; import { Button } from "~/components/ui/button/button"; export function ButtonSizesDemo() { return (
); } ``` ### Custom Variants ## Component Preview: button-variants-demo ```tsx import { Button } from "~/components/ui/button/button"; export function ButtonVariantsDemo() { return (
); } ``` ### Icon ## Component Preview: button-icon-demo ```tsx import { SearchIcon } from "lucide-react"; import { Button } from "~/components/ui/button/button"; export function ButtonIconDemo() { return ( ); } ``` Icon-only buttons need accessible labels. Choose your preferred approach: ```tsx // Option 1: aria-label (concise) // [!code word:aria-label="Search"] // Option 2: visually hidden text ``` Both work identically. Use `aria-label` for brevity or `sr-only` for visibility in your code. ## API Reference # Calendar (/docs/components/calendar) --- title: Calendar description: A calendar displays one or more date grids and allows users to select a single date. links: docs: https://react-aria.adobe.com/Calendar --- ## Component Preview: calendar-demo ```tsx "use client"; import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; import { Button } from "~/components/ui/button/button"; import { Calendar } from "~/components/ui/calendar"; export function CalendarDemo() { return ( {(weekDay) => {weekDay}} {(date) => } ); } ``` ## Installation CLI Manual ```bash npx shadcn@latest add @kanpeki/calendar ``` Install the following dependencies: ```bash npm install @internationalized/date ``` Copy and paste the following code into your project. ## Component Source: calendar ```tsx // File: index.ts export * from "./calendar"; export * as Calendar from "./namespace"; export * from "./styles"; // File: calendar.tsx "use client"; import { getLocalTimeZone, isToday } from "@internationalized/date"; import type { VariantProps } from "cva"; import { Calendar, composeRenderProps, type DateValue, Heading, CalendarCell as RACCalendarCell, CalendarGrid as RACCalendarGrid, CalendarGridBody as RACCalendarGridBody, CalendarGridHeader as RACCalendarGridHeader, CalendarHeaderCell as RACCalendarHeaderCell, RangeCalendar, } from "react-aria-components"; import { CalendarStyles } from "./styles"; export interface CalendarRootProps extends React.ComponentProps>, VariantProps {} export function CalendarRoot({ className, variant, ...props }: CalendarRootProps) { return ( CalendarStyles.Root({ className, variant, }) )} data-slot="calendar-root" {...props} /> ); } export interface RangeCalendarRootProps extends React.ComponentProps>, VariantProps {} export function RangeCalendarRoot({ className, variant, ...props }: RangeCalendarRootProps) { return ( CalendarStyles.Root({ className, variant, }) )} data-slot="range-calendar-root" {...props} /> ); } export interface CalendarHeaderProps extends React.ComponentProps<"header"> {} export function CalendarHeader({ className, ...props }: CalendarHeaderProps) { return (
); } export interface CalendarMonthProps extends React.ComponentProps {} export function CalendarMonth({ className, ...props }: CalendarMonthProps) { return ( ); } export interface CalendarNavProps extends React.ComponentProps<"nav"> {} export function CalendarNav({ className, ...props }: CalendarNavProps) { return (