BLAST UI

Tabs

Tabs is a component that displays multiple tabs.

Code

import { AnimatePresence, motion } from "motion/react";
import React from "react";

export interface TabsContext {
	selected: string;
	onTrigger: (name: string) => void;
	isFirstRender: boolean;
}

export const TabsContext = React.createContext<TabsContext>({} as TabsContext);

export function useTabs() {
	return React.useContext(TabsContext);
}

export interface TabsProps {
	defaultValue: string;
	children: React.ReactNode;
	onTrigger?: (name: string) => void;
}

export function Tabs({ children, defaultValue, onTrigger }: TabsProps) {
	const [selected, setSelected] = React.useState(defaultValue);
	const isFirstRender = React.useRef(true);

	function handleTrigger(name: string) {
		isFirstRender.current = false;
		setSelected(name);
		onTrigger?.(name);
	}

	return (
		<TabsContext.Provider
			value={{
				isFirstRender: isFirstRender.current,
				selected,
				onTrigger: handleTrigger,
			}}
		>
			{children}
		</TabsContext.Provider>
	);
}

export interface TabsPanelProps extends React.ComponentProps<"div"> {}

export function TabsPanel(props: TabsPanelProps) {
	return <div {...props} />;
}

export interface TabTriggerProps extends React.ComponentProps<"button"> {
	trigger: string;
}

export function TabTrigger(props: TabTriggerProps) {
	const { onTrigger, selected } = useTabs();

	return (
		<button
			{...props}
			onClick={e => {
				onTrigger(props.trigger);
				props.onClick?.(e);
			}}
			data-selected={selected === props.trigger}
		/>
	);
}

export interface TabsContent extends React.ComponentProps<"div"> {}

export function TabsContent({ children, ...props }: TabsContent) {
	const { selected, isFirstRender } = useTabs();
	return (
		<div {...props}>
			<AnimatePresence mode="wait">
				{React.Children.map(children, child => {
					if (!React.isValidElement<TabItem>(child)) return null;

					if (child.props.name === selected) {
						return React.cloneElement(child, {
							initial: isFirstRender ? false : child.props.initial,
						});
					}
					return null;
				})}
			</AnimatePresence>
		</div>
	);
}

export interface TabItem extends React.ComponentProps<typeof motion.div> {
	children: React.ReactNode;
	name: string;
}

export function TabItem({ children, name, ...props }: TabItem) {
	return <motion.div {...props}>{children}</motion.div>;
}

Example

Tab 1 content