The Coline design system: accessible React components for Coline Apps, themed live by the host workspace, and for Coline Sites in light and dark. Tailwind v4 stylesheet included. MIT.
| 5 |
| import { Tabs as TabsPrimitive } from "radix-ui" |
| 6 | import { motion } from "framer-motion" |
| 7 |
| 8 | import { cn } from "../lib/utils" |
| 9 |
| 10 | /** Shared spring for tab pill + trigger layout — keep in sync wherever `layout` runs alongside `TabsIndicator`. */ |
| 11 | export const TABS_LAYOUT_SPRING = { |
| 12 | type: "spring" as const, |
| 13 | stiffness: 720, |
| 14 | damping: 46, |
| 15 | mass: 0.48, |
| 16 | } |
| 17 |
| 18 | function Tabs({ |
| 19 | className, |
| 20 | orientation = "horizontal", |
| 21 | ...props |
| 22 | }: React.ComponentProps<typeof TabsPrimitive.Root>) { |
| 23 | return ( |
| 24 | <TabsPrimitive.Root |
| 25 | data-slot="tabs" |
| 26 | data-orientation={orientation} |
| 27 | className={cn( |
| 28 | "group/tabs flex gap-2 data-horizontal:flex-col", |
| 29 | className |
| 30 | )} |
| 31 | {...props} |
| 32 | /> |
| 33 | ) |
| 34 | } |
| 35 |
| 36 | const tabsListVariants = cva( |
| 37 | "group/tabs-list relative inline-flex w-fit items-center justify-center rounded-4xl p-[3px] text-muted-foreground group-data-horizontal/tabs:h-9 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col group-data-vertical/tabs:rounded-2xl data-[variant=line]:rounded-none", |
| 38 | { |
| 39 | variants: { |
| 40 | variant: { |
| 41 | default: |
| 42 | "bg-muted/70 border border-black/4 shadow-[inset_0_1px_2px_rgba(0,0,0,0.06)] dark:bg-white/4 dark:border-white/4 dark:shadow-[inset_0_1px_3px_rgba(0,0,0,0.3)]", |
| 43 | line: "gap-1 bg-transparent", |
| 44 | }, |
| 45 | }, |
| 46 | defaultVariants: { |
| 47 | variant: "default", |
| 48 | }, |
| 49 | } |
| 50 | ) |
| 51 |
| 52 | function TabsList({ |
| 53 | className, |
| 54 | variant = "default", |
| 55 | ...props |
| 56 | }: React.ComponentProps<typeof TabsPrimitive.List> & |
| 57 | VariantProps<typeof tabsListVariants>) { |
| 58 | return ( |
| 59 | <TabsPrimitive.List |
| 60 | data-slot="tabs-list" |
| 61 | data-variant={variant} |
| 62 | className={cn(tabsListVariants({ variant }), className)} |
| 63 | {...props} |
| 64 | /> |
| 65 | ) |
| 66 | } |
| 67 |
| 68 | function TabsTrigger({ |
| 69 | className, |
| 70 | ...props |
| 71 | }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { |
| 72 | return ( |
| 73 | <TabsPrimitive.Trigger |
| 74 | data-slot="tabs-trigger" |
| 75 | className={cn( |
| 76 | "relative z-1 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/50 transition-[color,background-color,border-color] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start group-data-vertical/tabs:px-2.5 group-data-vertical/tabs:py-1.5 hover:text-foreground/80 focus-visible:border-ring disabled:pointer-events-none disabled:opacity-50 dark:text-muted-foreground/70 dark:hover:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", |
| 77 | "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=line]/tabs-list:data-active:shadow-none dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", |
| 78 | "data-active:text-foreground data-active:hover:text-foreground dark:data-active:text-foreground dark:data-active:hover:text-foreground", |
| 79 | "after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:-bottom-1.25 group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", |
| 80 | className |
| 81 | )} |
| 82 | {...props} |
| 83 | /> |
| 84 | ) |
| 85 | } |
| 86 |
| 87 | /** Animated sliding pill indicator for TabsList (Apple-style). */ |
| 88 | function TabsIndicator({ |
| 89 | className, |
| 90 | layoutId = "tabs-indicator", |
| 91 | }: { |
| 92 | className?: string |
| 93 | layoutId?: string |
| 94 | }) { |
| 95 | return ( |
| 96 | <motion.div |
| 97 | layout="position" |
| 98 | layoutId={layoutId} |
| 99 | className={cn( |
| 100 | "absolute inset-0 z-[-1] rounded-xl bg-background border border-black/6 shadow-[0_1px_3px_0_rgba(0,0,0,0.08)] dark:bg-white/7 dark:border-white/6 dark:shadow-[0_1px_3px_0_rgba(0,0,0,0.3),inset_0_1px_0_0_rgba(255,255,255,0.04)]", |
| 101 | className |
| 102 | )} |
| 103 | transition={TABS_LAYOUT_SPRING} |
| 104 | /> |
| 105 | ) |
| 106 | } |
| 107 |
| 108 | function TabsContent({ |
| 109 | className, |
| 110 | ...props |
| 111 | }: React.ComponentProps<typeof TabsPrimitive.Content>) { |
| 112 | return ( |
| 113 | <TabsPrimitive.Content |
| 114 | data-slot="tabs-content" |
| 115 | className={cn( |
| 116 | "flex-1 text-sm outline-none animate-in fade-in-0 slide-in-from-bottom-0.5 duration-100 ease-out", |
| 117 | className |
| 118 | )} |
| 119 | {...props} |
| 120 | /> |
| 121 | ) |
| 122 | } |
| 123 |
| 124 | export { Tabs, TabsList, TabsTrigger, TabsIndicator, TabsContent, tabsListVariants } |
| 125 |