Open FilipePfluck opened 1 year ago
Hi, I also provide my implementation which keep in mind reusibility and flexibility using custom
attribute from framer-motion and expose a method resolveDirection
on top of my Tabs component.
Built with ts
, tailwind
and framer-motion
on top of shadcn/ui
tabs component.
type TabsContext = {
value: string;
setValue: (value: string) => void;
getDirection: (value: string) => -1 | 0 | 1;
};
const TabsContext = createContext<TabsContext>({
value: '',
setValue: () => {},
getDirection: () => 0,
});
const useTabsContext = () => {
const context = useContext(TabsContext);
if (!context) {
throw new Error(
'Tabs compound components cannot be rendered outside the Tabs component',
);
}
return context;
};
type TabsProps = ComponentPropsWithoutRef<typeof TabsPrimitive.Root> & {
resolveDirection: (selected: string, current: string) => -1 | 0 | 1;
};
const Tabs = forwardRef<ElementRef<typeof TabsPrimitive.Root>, TabsProps>(
({ resolveDirection, ...props }, ref) => {
const [selectedItem = '', setSelectedItem] = useControllableState<string>({
prop: props.value,
defaultProp: props.defaultValue,
onChange: props.onValueChange,
});
const getDirection = useCallback(
(current: string) => resolveDirection(selectedItem, current),
[selectedItem, resolveDirection],
);
return (
<TabsContext.Provider
value={useMemo(
() => ({
value: selectedItem,
setValue: setSelectedItem,
getDirection,
}),
[selectedItem, setSelectedItem, getDirection],
)}>
<TabsPrimitive.Root
ref={ref}
{...props}
value={selectedItem}
onValueChange={setSelectedItem}
className={'overflow-hidden'}
/>
</TabsContext.Provider>
);
},
);
Tabs.displayName = TabsPrimitive.Root.displayName;
const TabsContentVariants = {
hidden: (direction: -1 | 0 | 1) => ({
x: direction * 382,
opacity: 0,
}),
visible: () => ({
opacity: 1,
x: 0,
}),
};
const TabsContent = forwardRef<
ElementRef<typeof TabsPrimitive.Content>,
ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, children, ...props }, ref) => {
const { value, getDirection } = useTabsContext();
const active = value === props.value;
const direction = useMemo(() => getDirection(props.value), [getDirection, props.value]);
return (
<AnimatePresence initial={false}>
{active ? (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-2 ring-offset-background',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className,
)}
forceMount
{...props}
asChild>
<motion.div
custom={direction}
variants={TabsContentVariants}
animate={active ? 'visible' : 'hidden'}
initial={active ? 'visible' : 'hidden'}
exit={'hidden'}>
{children}
</motion.div>
</TabsPrimitive.Content>
) : null}
</AnimatePresence>
);
});
Add data-motion to Tabs component
Overview
I created a tab component with animations when switching tabs. The tab enters and exits from the right or from the left, based on the direction the new tab is. I use framer motion to do this. I see that in the navigation-menu there is something similar using just css. It uses data-motion to determine which animation should be used. I wonder if something similar could be implemented in the tabs component?
Examples in other libraries
https://www.radix-ui.com/docs/primitives/components/navigation-menu
Who does this impact? Who is this for?
For anyone wanting to make more complex animations without necessarily using other tools for this.
Additional context
If anyone is curious to see how I implemented my tabs component, it is here: https://github.com/FilipePfluck/my-components/tree/main/src/components/Tabs