Animated Empty State
Build an exit animation for empty states using CSS transitions.
Get started by adding a new tag.
In this use case we have a tag group that is initially empty. For design reasons, we want to keep empty states consistent and can't show the combo box which adds tags at the beginning. With a button press, the empty state becomes smaller and reveals the input.
This is a real world example that has been debranded for the purpose of this recipe. It uses React Aria Components for all interactive elements as well as the tag group that shows the empty state.
Best Practices
Prefix React Aria Components' imports to avoid confusion with custom components.
import { Button as AriaButton } from 'react-aria-components';Access Tailwind CSS' theme with arbitrary values.
showInput ? 'h-[calc(theme(spacing.9)+theme(spacing.4))]' : 'h-0',Easily style children inside the parent using
data-slot
.'[&>[data-slot=icon]]:-mx-0.5 [&>[data-slot=icon]]:size-4',Group styles by splitting them into multiple lines with
clsx
.className={clsx(// Button'flex items-center gap-x-1.5 ...',// Icon'[&>[data-slot=icon]]:-mx-0.5 [&>[data-slot=icon]]:size-4',)}
Requirements
Code
import { PlusIcon } from '@heroicons/react/16/solid';import { FolderPlusIcon } from '@heroicons/react/24/outline';import { clsx } from 'clsx';import { type ComponentProps, useState } from 'react';import { Button as AriaButton, type ButtonProps as AriaButtonProps, Input as AriaInput, type InputProps as AriaInputProps, TagGroup as AriaTagGroup, TagList as AriaTagList,} from 'react-aria-components';
interface EmptyTagGroupProps { showInput: boolean; setShowInput: Dispatch<SetStateAction<boolean>>;}
export function AnimatedEmptyStateExample() { const [showInput, setShowInput] = useState(false);
return ( <div className="space-y-2"> <div className="text-base font-semibold leading-6 text-gray-900"> Tags </div>
<Card className="w-96"> <div className={clsx( 'transition-all duration-300', showInput ? 'h-[calc(theme(spacing.9)+theme(spacing.4))]' : 'h-0', )} > <Input /> </div>
<AriaTagGroup aria-label="Tag group"> <AriaTagList renderEmptyState={() => ( <EmptyTagGroup showInput={showInput} setShowInput={setShowInput} /> )} /> </AriaTagGroup> </Card> </div> );}
function Card({ children, className = '', ...props}: ComponentProps<'div'>) { return ( <div className={cx('rounded-lg bg-white p-6 shadow', className)} {...props}> {children} </div> );}
function EmptyTagGroup({ showInput, setShowInput }: EmptyTagGroupProps) { return ( <div className={clsx( 'flex flex-col items-center text-center transition-all duration-300', showInput ? 'rounded-md bg-gray-100 p-4' : '-m-6 rounded-lg bg-gray-200 p-6', )} > <div className="rounded bg-white p-2 shadow-sm"> <FolderPlusIcon className="size-6" /> </div>
<div className="mt-3 text-sm font-medium text-gray-900">No tags</div>
<p className="mt-1 text-sm text-gray-500"> Get started by adding a new tag. </p>
<div className={clsx( 'grid place-items-end overflow-hidden transition-all duration-300', showInput ? 'h-0' : 'h-12', )} > <Button onPress={() => setShowInput(true)}> <PlusIcon /> Add Tag </Button> </div> </div> );}
function Button({ children, ...props}: Omit<AriaButtonProps, 'className'>) { return ( <AriaButton className={clsx( 'flex items-center gap-x-1.5 rounded-md bg-emerald-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-emerald-700 focus-visible:outline-none', '[&>[data-slot=icon]]:-mx-0.5 [&>[data-slot=icon]]:size-4', )} {...props} > {children} </AriaButton> );}
function Input({ ...props }: Omit<AriaInputProps, 'className'>) { return ( <AriaInput className="block w-full rounded-md border-0 py-1.5 text-sm leading-6 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-emerald-600" {...props} /> );}