Back

Animated Empty State

Build an exit animation for empty states using CSS transitions.

Tags
No tags

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}
/>
);
}