Empty
Use the Empty component to display an empty state.
No Projects Yet
You haven't created any projects yet. Get started by creating your first project.
Learn More
import { css } from 'remix/component'
import { Button, buttonVariants } from '#app/ui/button.tsx'
import {
Empty,
EmptyContent,import { css } from 'remix/component'
import { Button, buttonVariants } from '#app/ui/button.tsx'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '#app/ui/empty.tsx'
import { ArrowUpRightIcon, FolderCodeIcon } from '#app/ui/icons.tsx'
const actionsStyles = css({ display: 'flex', gap: '0.5rem' })
const learnMoreStyles = css({ color: 'var(--muted-foreground)' })
export function EmptyDemo() {
return () => (
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<FolderCodeIcon />
</EmptyMedia>
<EmptyTitle>No Projects Yet</EmptyTitle>
<EmptyDescription>
You haven't created any projects yet. Get started by creating your first project.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<div mix={actionsStyles}>
<Button>Create Project</Button>
<Button variant="outline">Import Project</Button>
</div>
</EmptyContent>
<a href="#" mix={[buttonVariants({ variant: 'link', size: 'sm' }), learnMoreStyles]}>
Learn More <ArrowUpRightIcon />
</a>
</Empty>
)
}Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/empty.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/empty.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/empty.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/empty.jsonInstall the following dependencies:
$ bun add remix$ npm install remix$ pnpm add remix$ yarn add remixCopy and paste the following code into your project.
import { css, type CSSMixinDescriptor, type Handle, type Props } from 'remix/component'
export type EmptyMediaVariant = 'default' | 'icon'
export interface EmptyMediaProps extends Props<'div'> {
variant?: EmptyMediaVariant
}
const emptyStyles = css({
display: 'flex',
minWidth: 0,
flex: '1 1 0%',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '1.5rem',
borderRadius: 'var(--radius)',
borderStyle: 'dashed',
padding: '1.5rem',
textAlign: 'center',
textWrap: 'balance',
'@media (min-width: 48rem)': { padding: '3rem' },
})
const headerStyles = css({
display: 'flex',
maxWidth: '24rem',
flexDirection: 'column',
alignItems: 'center',
gap: '0.5rem',
textAlign: 'center',
})
const mediaBase = css({
marginBottom: '0.5rem',
display: 'flex',
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
'& svg': { pointerEvents: 'none', flexShrink: 0 },
})
const mediaVariants: Record<EmptyMediaVariant, CSSMixinDescriptor> = {
default: css({ backgroundColor: 'transparent' }),
icon: css({
display: 'flex',
width: '2.5rem',
height: '2.5rem',
flexShrink: 0,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'var(--radius)',
backgroundColor: 'var(--muted)',
color: 'var(--foreground)',
'& svg:not([class*="size-"])': { width: '1.5rem', height: '1.5rem' },
}),
}
const titleStyles = css({
fontSize: '1.125rem',
lineHeight: 'calc(1.75 / 1.125)',
fontWeight: 500,
letterSpacing: '-0.025em',
})
const descriptionStyles = css({
fontSize: '0.875rem',
lineHeight: 1.625,
color: 'var(--muted-foreground)',
'& > a': { textDecorationLine: 'underline', textUnderlineOffset: '4px' },
'& > a:hover': { color: 'var(--primary)' },
})
const contentStyles = css({
display: 'flex',
width: '100%',
maxWidth: '24rem',
minWidth: 0,
flexDirection: 'column',
alignItems: 'center',
gap: '1rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
textWrap: 'balance',
})
export function Empty(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="empty" {...props} mix={[emptyStyles, mix]} />
}
}
export function EmptyHeader(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="empty-header" {...props} mix={[headerStyles, mix]} />
}
}
/** The `emptyMediaVariants()` of shadcn/ui, as a mix. */
export function emptyMediaVariants({ variant = 'default' }: { variant?: EmptyMediaVariant } = {}) {
return [mediaBase, mediaVariants[variant]]
}
export function EmptyMedia(handle: Handle<EmptyMediaProps>) {
return () => {
let { variant = 'default', mix, ...props } = handle.props
return (
<div
data-slot="empty-icon"
data-variant={variant}
{...props}
mix={[emptyMediaVariants({ variant }), mix]}
/>
)
}
}
export function EmptyTitle(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="empty-title" {...props} mix={[titleStyles, mix]} />
}
}
export function EmptyDescription(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="empty-description" {...props} mix={[descriptionStyles, mix]} />
}
}
export function EmptyContent(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return <div data-slot="empty-content" {...props} mix={[contentStyles, mix]} />
}
}Update the import paths to match your project setup.
Usage
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '#app/ui/empty.tsx'<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<Icon />
</EmptyMedia>
<EmptyTitle>No data</EmptyTitle>
<EmptyDescription>No data found</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>Add data</Button>
</EmptyContent>
</Empty>Examples
Outline
Use the border utility class to create an outline empty state.
Cloud Storage Empty
Upload files to your cloud storage to access them anywhere.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import {
Empty,
EmptyContent,import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '#app/ui/empty.tsx'
import { CloudIcon } from '#app/ui/icons.tsx'
const outlineStyles = css({ borderWidth: '1px', borderStyle: 'dashed' })
export function EmptyOutline() {
return () => (
<Empty mix={outlineStyles}>
<EmptyHeader>
<EmptyMedia variant="icon">
<CloudIcon />
</EmptyMedia>
<EmptyTitle>Cloud Storage Empty</EmptyTitle>
<EmptyDescription>Upload files to your cloud storage to access them anywhere.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm">
Upload Files
</Button>
</EmptyContent>
</Empty>
)
}Background
Use the bg-* and bg-gradient-* utilities to add a background to the empty state.
No Notifications
You're all caught up. New notifications will appear here.
import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import {
Empty,
EmptyContent,import { css } from 'remix/component'
import { Button } from '#app/ui/button.tsx'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '#app/ui/empty.tsx'
import { BellIcon, RefreshCcwIcon } from '#app/ui/icons.tsx'
const backgroundStyles = css({
height: '100%',
backgroundImage:
'linear-gradient(to bottom in oklab, color-mix(in oklab, var(--muted) 50%, transparent) 30%, var(--background) 100%)',
})
export function EmptyBackground() {
return () => (
<Empty mix={backgroundStyles}>
<EmptyHeader>
<EmptyMedia variant="icon">
<BellIcon />
</EmptyMedia>
<EmptyTitle>No Notifications</EmptyTitle>
<EmptyDescription>
You're all caught up. New notifications will appear here.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm">
<RefreshCcwIcon />
Refresh
</Button>
</EmptyContent>
</Empty>
)
}Avatar
Use the EmptyMedia component to display an avatar in the empty state.
LR
User Offline
This user is currently offline. You can leave a message to notify them or try again later.
import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { Button } from '#app/ui/button.tsx'
import {
Empty,import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { Button } from '#app/ui/button.tsx'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '#app/ui/empty.tsx'
const avatarStyles = css({ width: '3rem', height: '3rem' })
const grayscale = css({ filter: 'grayscale(100%)' })
export function EmptyAvatar() {
return () => (
<Empty>
<EmptyHeader>
<EmptyMedia variant="default">
<Avatar mix={avatarStyles}>
<AvatarImage src="https://github.com/shadcn.png" mix={grayscale} />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
</EmptyMedia>
<EmptyTitle>User Offline</EmptyTitle>
<EmptyDescription>
This user is currently offline. You can leave a message to notify them or try again later.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">Leave Message</Button>
</EmptyContent>
</Empty>
)
}Avatar Group
Use the EmptyMedia component to display an avatar group in the empty state.
CNLRER
No Team Members
Invite your team to collaborate on this project.
import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { Button } from '#app/ui/button.tsx'
import {
Empty,import { css } from 'remix/component'
import { Avatar, AvatarFallback, AvatarImage } from '#app/ui/avatar.tsx'
import { Button } from '#app/ui/button.tsx'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '#app/ui/empty.tsx'
import { PlusIcon } from '#app/ui/icons.tsx'
const groupStyles = css({
display: 'flex',
'& > :not(:last-child)': { marginInlineEnd: '-0.5rem' },
})
// `*:data-[slot=avatar]:size-12 ring-2 ring-background grayscale`, applied to each avatar so it
// layers after the avatar's own styles.
const avatarStyles = css({
width: '3rem',
height: '3rem',
boxShadow: '0 0 0 2px var(--background)',
filter: 'grayscale(100%)',
})
export function EmptyAvatarGroup() {
return () => (
<Empty>
<EmptyHeader>
<EmptyMedia>
<div mix={groupStyles}>
<Avatar mix={avatarStyles}>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar mix={avatarStyles}>
<AvatarImage src="https://github.com/maxleiter.png" alt="@maxleiter" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar mix={avatarStyles}>
<AvatarImage src="https://github.com/evilrabbit.png" alt="@evilrabbit" />
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</div>
</EmptyMedia>
<EmptyTitle>No Team Members</EmptyTitle>
<EmptyDescription>Invite your team to collaborate on this project.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">
<PlusIcon />
Invite Members
</Button>
</EmptyContent>
</Empty>
)
}InputGroup
You can add an InputGroup component to the EmptyContent component.
404 - Not Found
The page you're looking for doesn't exist. Try searching for what you need below.
/
Need help? Contact support
import { css } from 'remix/component'
import {
Empty,
EmptyContent,
EmptyDescription,import { css } from 'remix/component'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyTitle,
} from '#app/ui/empty.tsx'
import { SearchIcon } from '#app/ui/icons.tsx'
import { InputGroup, InputGroupAddon, InputGroupInput } from '#app/ui/input-group.tsx'
import { Kbd } from '#app/ui/kbd.tsx'
const inputGroupStyles = css({ '@media (min-width: 40rem)': { width: '75%' } })
export function EmptyInputGroup() {
return () => (
<Empty>
<EmptyHeader>
<EmptyTitle>404 - Not Found</EmptyTitle>
<EmptyDescription>
The page you're looking for doesn't exist. Try searching for what you need below.
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<InputGroup mix={inputGroupStyles}>
<InputGroupInput placeholder="Try searching for pages..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<Kbd>/</Kbd>
</InputGroupAddon>
</InputGroup>
<EmptyDescription>
Need help? <a href="#">Contact support</a>
</EmptyDescription>
</EmptyContent>
</Empty>
)
}Icon
No messages
Your inbox is empty. New messages will appear here.
No favorites
Items you mark as favorites will appear here.
No likes yet
Content you like will be saved here for easy access.
No bookmarks
Save interesting content by bookmarking it.
import { css } from 'remix/component'
import {
Empty,
EmptyDescription,
EmptyHeader,import { css } from 'remix/component'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '#app/ui/empty.tsx'
import { BookmarkIcon, HeartIcon, InboxIcon, StarIcon } from '#app/ui/icons.tsx'
const gridStyles = css({
display: 'grid',
gap: '2rem',
'@media (min-width: 48rem)': { gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' },
})
export function EmptyIcon() {
return () => (
<div mix={gridStyles}>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<InboxIcon />
</EmptyMedia>
<EmptyTitle>No messages</EmptyTitle>
<EmptyDescription>Your inbox is empty. New messages will appear here.</EmptyDescription>
</EmptyHeader>
</Empty>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<StarIcon />
</EmptyMedia>
<EmptyTitle>No favorites</EmptyTitle>
<EmptyDescription>Items you mark as favorites will appear here.</EmptyDescription>
</EmptyHeader>
</Empty>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<HeartIcon />
</EmptyMedia>
<EmptyTitle>No likes yet</EmptyTitle>
<EmptyDescription>Content you like will be saved here for easy access.</EmptyDescription>
</EmptyHeader>
</Empty>
<Empty>
<EmptyHeader>
<EmptyMedia variant="icon">
<BookmarkIcon />
</EmptyMedia>
<EmptyTitle>No bookmarks</EmptyTitle>
<EmptyDescription>Save interesting content by bookmarking it.</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
)
}API Reference
Empty
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
EmptyHeader
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
EmptyMedia
| Prop | Type | Default |
|---|---|---|
variant | EmptyMediaVariant | "default" |
mix | MixValue | - |
EmptyTitle
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
EmptyDescription
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
EmptyContent
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |