Toggle Group
toggle-group ui Group of `Toggle` buttons that act as a single-select or multi-select control. Use for view-mode pickers (grid/list), text-format toolbars, and any "pick one of N" button bar.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/toggle-group.json $ npx shadcn@latest add https://uipkge.dev/r/react/toggle-group.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/toggle-group.json $ bunx shadcn@latest add https://uipkge.dev/r/react/toggle-group.json Named registry:
npx shadcn@latest add @uipkge-react/toggle-group Installs to: components/ui/toggle-group/ Examples
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases from this item's source — use them to shape the data you pass in.
ToggleGroupContextValue type ToggleGroupContextValue {
variant?: ToggleVariants['variant']
size?: ToggleVariants['size']
spacing?: number
} npm dependencies
Includes
Used by
Files installed (2)
-
components/ui/toggle-group/toggle-group.tsx 7.1 kB
'use client' import * as React from 'react' import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group' import { cn } from '@/lib/utils' import { toggleVariants, type ToggleVariants } from '../toggle/toggle.variants' // Make variant/size/spacing reachable from items without each consumer having // to pass it manually — the React equivalent of the Vue provide('toggleGroup'). type ToggleGroupContextValue = { variant?: ToggleVariants['variant'] size?: ToggleVariants['size'] spacing?: number } const ToggleGroupContext = React.createContext<ToggleGroupContextValue>({}) export type ToggleGroupProps = React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> & ToggleVariants & { spacing?: number /** Sliding selection indicator for single-select (default true). Multi-select keeps item chrome. */ animated?: boolean } const ToggleGroup = React.forwardRef<React.ElementRef<typeof ToggleGroupPrimitive.Root>, ToggleGroupProps>( ({ className, variant, size, spacing = 0, animated = true, children, type, ...props }, ref) => { // Sliding pill only for single-select. Multi-select paints per-item surfaces. const indicatorActive = animated !== false && type !== 'multiple' const listRef = React.useRef<HTMLDivElement | null>(null) const firstPosition = React.useRef(true) const [indicatorStyle, setIndicatorStyle] = React.useState<React.CSSProperties>({ opacity: 0 }) const setRefs = React.useCallback( (node: HTMLDivElement | null) => { listRef.current = node if (typeof ref === 'function') ref(node) else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node }, [ref], ) const motionSafeTransition = React.useCallback(() => { if (typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) { return 'none' } return firstPosition.current ? 'none' : 'transform 220ms cubic-bezier(0.22, 1, 0.36, 1), width 220ms cubic-bezier(0.22, 1, 0.36, 1), height 220ms cubic-bezier(0.22, 1, 0.36, 1), border-radius 220ms cubic-bezier(0.22, 1, 0.36, 1)' }, []) const updateIndicator = React.useCallback(() => { if (!indicatorActive) return const root = listRef.current if (!root) return const active = root.querySelector<HTMLElement>('[data-slot="toggle-group-item"][data-state="on"]') if (!active) { setIndicatorStyle({ opacity: 0 }) return } const listRect = root.getBoundingClientRect() const activeRect = active.getBoundingClientRect() const left = activeRect.left - listRect.left + root.scrollLeft const top = activeRect.top - listRect.top + root.scrollTop const transition = motionSafeTransition() setIndicatorStyle({ width: activeRect.width, height: activeRect.height, transform: `translate3d(${left}px, ${top}px, 0)`, borderRadius: getComputedStyle(active).borderRadius, opacity: 1, transition, }) firstPosition.current = false }, [indicatorActive, motionSafeTransition]) // Bind observers once per animated mode / layout props — NOT on every children // identity change (controlled groups re-render parents constantly). React.useLayoutEffect(() => { firstPosition.current = true if (!indicatorActive) { setIndicatorStyle({ opacity: 0 }) return } updateIndicator() const root = listRef.current if (!root) return const ro = new ResizeObserver(() => updateIndicator()) ro.observe(root) root.querySelectorAll('[data-slot="toggle-group-item"]').forEach((el) => ro.observe(el)) const mo = new MutationObserver((mutations) => { for (const m of mutations) { if (m.type === 'childList') { root.querySelectorAll('[data-slot="toggle-group-item"]').forEach((el) => ro.observe(el)) } } updateIndicator() }) mo.observe(root, { attributes: true, attributeFilter: ['data-state'], subtree: true, childList: true, }) return () => { ro.disconnect() mo.disconnect() } }, [indicatorActive, size, spacing, updateIndicator, variant]) return ( <ToggleGroupPrimitive.Root ref={setRefs} type={type} data-uipkge="" data-slot="toggle-group" data-size={size} data-variant={variant} data-spacing={spacing} data-animated={indicatorActive ? 'true' : 'false'} style={{ '--gap': spacing } as React.CSSProperties} className={cn( 'group/toggle-group relative flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs', className, )} {...props} > {indicatorActive && ( <span data-slot="toggle-group-indicator" aria-hidden="true" className="pointer-events-none absolute top-0 left-0 z-0 bg-accent shadow-xs will-change-transform" style={indicatorStyle} /> )} <ToggleGroupContext.Provider value={{ variant, size, spacing }}>{children}</ToggleGroupContext.Provider> </ToggleGroupPrimitive.Root> ) }, ) ToggleGroup.displayName = 'ToggleGroup' export type ToggleGroupItemProps = React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & ToggleVariants const ToggleGroupItem = React.forwardRef<React.ElementRef<typeof ToggleGroupPrimitive.Item>, ToggleGroupItemProps>( ({ className, variant, size, children, ...props }, ref) => { const context = React.useContext(ToggleGroupContext) return ( <ToggleGroupPrimitive.Item ref={ref} data-uipkge="" data-slot="toggle-group-item" data-variant={context.variant || variant} data-size={context.size || size} data-spacing={context.spacing} className={cn( toggleVariants({ variant: context.variant || variant, size: context.size || size, }), // z-10 keeps label/icons above the sliding indicator. When the parent // has data-animated=true (single-select), on-state surface lives on the // indicator — suppress item bg so the pill can slide cleanly. 'relative z-10 w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10', 'group-data-[animated=true]/toggle-group:data-[state=on]:bg-transparent group-data-[animated=true]/toggle-group:data-[state=on]:hover:bg-transparent', // first/last-of-type (not first/last-child): sliding indicator is a sibling span // and must not steal end-cap rounding or the outline left border. 'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first-of-type:rounded-l-md data-[spacing=0]:last-of-type:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first-of-type:border-l', className, )} {...props} > {children} </ToggleGroupPrimitive.Item> ) }, ) ToggleGroupItem.displayName = 'ToggleGroupItem' export { ToggleGroup, ToggleGroupItem } -
components/ui/toggle-group/index.ts 0.1 kB
export { ToggleGroup, ToggleGroupItem, type ToggleGroupProps, type ToggleGroupItemProps } from './toggle-group'
Raw manifest: https://uipkge.dev/r/react/toggle-group.json