Empty
What to show when there is nothing yet. Icon media, title, description and actions; dashed outline.
import { Button } from "@/components/ui/button";import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle,} from "@/components/ui/empty";import { IconPlaceholder } from "@/edmi/icon-placeholder";
export default function Demo() { return ( <div className="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <Empty> <EmptyHeader> <EmptyMedia variant="icon"> <IconPlaceholder lucide="BriefcaseIcon" tabler="IconBriefcase" hugeicons="Briefcase01Icon" phosphor="BriefcaseIcon" remixicon="RiBriefcaseLine" /> </EmptyMedia> <EmptyTitle>No positions yet</EmptyTitle> <EmptyDescription> Join an index and your holdings will show up here. </EmptyDescription> </EmptyHeader> <EmptyContent> <Button>Explore indexes</Button> </EmptyContent> </Empty> <Empty className="border-[1.5px] border-input"> <EmptyHeader> <EmptyMedia variant="icon"> <IconPlaceholder lucide="UploadIcon" tabler="IconUpload" hugeicons="Upload01Icon" phosphor="UploadIcon" remixicon="RiUploadLine" /> </EmptyMedia> <EmptyTitle>Drop your weights file</EmptyTitle> <EmptyDescription> CSV with symbol and weight columns. </EmptyDescription> </EmptyHeader> <EmptyContent> <Button variant="outline">Choose file</Button> </EmptyContent> </Empty> </div> );}<script setup lang="ts">import { Building2Icon, UploadIcon } from "@lucide/vue";import { Button } from "@/components/ui/button";import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle,} from "@/components/ui/empty";</script>
<template> <div class="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <Empty> <EmptyHeader> <EmptyMedia variant="icon"><Building2Icon /></EmptyMedia> <EmptyTitle>No positions yet</EmptyTitle> <EmptyDescription>Join an index and your holdings will show up here.</EmptyDescription> </EmptyHeader> <EmptyContent> <Button>Explore indexes</Button> </EmptyContent> </Empty> <Empty class="border-[1.5px] border-input"> <EmptyHeader> <EmptyMedia variant="icon"><UploadIcon /></EmptyMedia> <EmptyTitle>Drop your weights file</EmptyTitle> <EmptyDescription>CSV with symbol and weight columns.</EmptyDescription> </EmptyHeader> <EmptyContent> <Button variant="outline">Choose file</Button> </EmptyContent> </Empty> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import * as Empty from "$lib/components/ui/empty"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte";</script>
<div class="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <Empty.Root> <Empty.Header> <Empty.Media variant="icon"><IconPlaceholder lucide="BriefcaseIcon" tabler="IconBriefcase" hugeicons="Briefcase01Icon" phosphor="BriefcaseIcon" remixicon="RiBriefcaseLine" /></Empty.Media> <Empty.Title>No positions yet</Empty.Title> <Empty.Description>Join an index and your holdings will show up here.</Empty.Description> </Empty.Header> <Empty.Content> <Button>Explore indexes</Button> </Empty.Content> </Empty.Root> <Empty.Root class="border-[1.5px] border-input"> <Empty.Header> <Empty.Media variant="icon"><IconPlaceholder lucide="UploadIcon" tabler="IconUpload" hugeicons="Upload01Icon" phosphor="UploadIcon" remixicon="RiUploadLine" /></Empty.Media> <Empty.Title>Drop your weights file</Empty.Title> <Empty.Description>CSV with symbol and weight columns.</Empty.Description> </Empty.Header> <Empty.Content> <Button variant="outline">Choose file</Button> </Empty.Content> </Empty.Root></div>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/emptypnpm dlx shadcn@latest add @edmi-ui/emptyyarn dlx shadcn@latest add @edmi-ui/emptybunx shadcn@latest add @edmi-ui/emptyPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install class-variance-authorityTerminal window pnpm add class-variance-authorityTerminal window yarn add class-variance-authorityTerminal window bun add class-variance-authorityAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/emptypnpm dlx shadcn-vue@latest add @edmi-ui/emptyyarn dlx shadcn-vue@latest add @edmi-ui/emptybunx shadcn-vue@latest add @edmi-ui/emptyPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install class-variance-authorityTerminal window pnpm add class-variance-authorityTerminal window yarn add class-variance-authorityTerminal window bun add class-variance-authorityAdd the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/empty.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/empty.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/empty.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/empty.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Add the Edmi components this one builds on:
elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
import { Button } from "@/components/ui/button";import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle,} from "@/components/ui/empty";
<div className="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <Empty> <EmptyHeader> <EmptyMedia variant="icon"> <BriefcaseIcon /> </EmptyMedia> <EmptyTitle>No positions yet</EmptyTitle> <EmptyDescription> Join an index and your holdings will show up here. </EmptyDescription> </EmptyHeader> <EmptyContent> <Button>Explore indexes</Button> </EmptyContent> </Empty> {/* … */}</div><script setup lang="ts">import { Button } from "@/components/ui/button";import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle,} from "@/components/ui/empty";</script>
<template> <div class="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <Empty> <EmptyHeader> <EmptyMedia variant="icon"><Building2Icon /></EmptyMedia> <EmptyTitle>No positions yet</EmptyTitle> <EmptyDescription>Join an index and your holdings will show up here.</EmptyDescription> </EmptyHeader> <EmptyContent> <Button>Explore indexes</Button> </EmptyContent> </Empty> <Empty class="border-[1.5px] border-input"> <EmptyHeader> <EmptyMedia variant="icon"><UploadIcon /></EmptyMedia> <EmptyTitle>Drop your weights file</EmptyTitle> <EmptyDescription>CSV with symbol and weight columns.</EmptyDescription> </EmptyHeader> <EmptyContent> <Button variant="outline">Choose file</Button> </EmptyContent> </Empty> </div></template><script lang="ts"> import * as Empty from "$lib/components/ui/empty"; import { Button } from "$lib/components/ui/button";</script>
<div class="grid w-full max-w-2xl gap-5 sm:grid-cols-2"> <Empty.Root> <Empty.Header> <Empty.Media variant="icon"><BriefcaseIcon /></Empty.Media> <Empty.Title>No positions yet</Empty.Title> <Empty.Description>Join an index and your holdings will show up here.</Empty.Description> </Empty.Header> <Empty.Content> <Button>Explore indexes</Button> </Empty.Content> </Empty.Root> <Empty.Root class="border-[1.5px] border-input"> <Empty.Header> <Empty.Media variant="icon"><UploadIcon /></Empty.Media> <Empty.Title>Drop your weights file</Empty.Title> <Empty.Description>CSV with symbol and weight columns.</Empty.Description> </Empty.Header> <Empty.Content> <Button variant="outline">Choose file</Button> </Empty.Content> </Empty.Root></div>Component source
Section titled “Component source”"use client";
import { cva, type VariantProps } from "class-variance-authority";import { cn } from "cn";import type * as React from "react";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// Outline = dashed: add `border border-input` (or `border-[1.5px]`) via className; the dashed style is built in.function Empty({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="empty" className={cn( "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-[14px] border-dashed p-10 text-center text-balance", className, )} {...props} /> );}
function EmptyHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="empty-header" className={cn("flex max-w-sm flex-col items-center gap-1.5", className)} {...props} /> );}
const emptyMediaVariants = cva( "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { default: "bg-transparent", icon: "size-12 rounded-xl border border-border bg-card text-foreground [&_svg:not([class*='size-'])]:size-5", }, // ✦ depth (only affects variant="icon"): the media tile rises elevation: { flat: "", raised: "", floating: "" }, }, compoundVariants: [ { variant: "icon", elevation: "raised", class: "border-transparent shadow-raised", }, { variant: "icon", elevation: "floating", class: "border-transparent shadow-floating", }, ], defaultVariants: { variant: "default", elevation: "flat", }, },);
function EmptyMedia({ className, variant = "default", elevation, ...props}: React.ComponentProps<"div"> & Pick<VariantProps<typeof emptyMediaVariants>, "variant"> & { /** ✦ depth for variant="icon": raised +1 / floating +2 make the media tile rise. */ elevation?: Elevation; }) { const level = useElevation(elevation, "handle"); return ( <div data-slot="empty-icon" data-variant={variant} className={cn( emptyMediaVariants({ variant, elevation: level === "sunken" ? "flat" : level, className, }), )} {...props} /> );}
function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="empty-title" className={cn("text-[15px] font-semibold tracking-[-0.2px]", className)} {...props} /> );}
function EmptyDescription({ className, ...props}: React.ComponentProps<"div">) { return ( <div data-slot="empty-description" className={cn( "text-[13px]/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", className, )} {...props} /> );}
function EmptyContent({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="empty-content" className={cn( "flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance", className, )} {...props} /> );}
export { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle,};<script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="empty" :class="cn('flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-[14px] border-dashed p-10 text-center text-balance', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="empty-content" :class="cn('flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="empty-description" :class="cn('text-[13px]/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="empty-header" :class="cn('flex max-w-sm flex-col items-center gap-1.5', props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import type { EmptyMediaVariants } from '.'import { cn } from '@/registry/edmi/lib/utils'import { type Elevation, useElevation } from '@/registry/edmi/ui/elevation'import { emptyMediaVariants } from '.'
const props = withDefaults(defineProps<{ class?: HTMLAttributes['class'] variant?: EmptyMediaVariants['variant'] /** ✦ depth for variant="icon": raised +1 / floating +2 make the media tile rise */ elevation?: Elevation}>(), { elevation: undefined })
const level = useElevation(() => props.elevation, 'handle')</script>
<template> <div data-slot="empty-icon" :data-variant="variant" :class="cn(emptyMediaVariants({ variant, elevation: level === 'sunken' ? 'flat' : level }), props.class)" > <slot /> </div></template><script setup lang="ts">import type { HTMLAttributes } from 'vue'import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class']}>()</script>
<template> <div data-slot="empty-title" :class="cn('text-[15px] font-semibold tracking-[-0.2px]', props.class)" > <slot /> </div></template>import type { VariantProps } from "class-variance-authority";import { cva } from "class-variance-authority";
export { default as Empty } from "./Empty.vue";export { default as EmptyContent } from "./EmptyContent.vue";export { default as EmptyDescription } from "./EmptyDescription.vue";export { default as EmptyHeader } from "./EmptyHeader.vue";export { default as EmptyMedia } from "./EmptyMedia.vue";export { default as EmptyTitle } from "./EmptyTitle.vue";
export const emptyMediaVariants = cva( "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { default: "bg-transparent", icon: "size-12 rounded-xl border border-border bg-card text-foreground [&_svg:not([class*=size-])]:size-5", }, // ✦ depth (icon variant only): the media tile rises elevation: { flat: "", raised: "", floating: "" }, }, compoundVariants: [ { variant: "icon", elevation: "raised", class: "border-transparent shadow-raised", }, { variant: "icon", elevation: "floating", class: "border-transparent shadow-floating", }, ], defaultVariants: { variant: "default", elevation: "flat", }, },);export type EmptyMediaVariants = VariantProps<typeof emptyMediaVariants>;<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="empty-content" class={cn( "flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-sm text-balance", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="empty-description" class={cn( "text-[13px]/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="empty-header" class={cn( "flex max-w-sm flex-col items-center gap-1.5", className )} {...restProps}> {@render children?.()}</div><script lang="ts" module> import { tv, type VariantProps } from "tailwind-variants";
export const emptyMediaVariants = tv({ base: "mb-2 flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0", variants: { variant: { default: "bg-transparent", icon: "size-12 rounded-xl border border-border bg-card text-foreground [&_svg:not([class*='size-'])]:size-5", }, // ✦ depth (icon variant only): the media tile rises elevation: { flat: "", raised: "", floating: "" }, }, compoundVariants: [ { variant: "icon", elevation: "raised", class: "border-transparent shadow-raised" }, { variant: "icon", elevation: "floating", class: "border-transparent shadow-floating" }, ], defaultVariants: { variant: "default", elevation: "flat", }, });
export type EmptyMediaVariant = VariantProps<typeof emptyMediaVariants>["variant"];</script>
<script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, variant = "default", elevation = "auto", ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> & { variant?: EmptyMediaVariant; /** ✦ depth for variant="icon": raised +1 / floating +2 make the media tile rise. */ elevation?: Elevation; } = $props();
const level = useElevation(() => elevation, "handle");</script>
<div bind:this={ref} data-slot="empty-icon" data-variant={variant} class={cn(emptyMediaVariants({ variant, elevation: level.current === "sunken" ? "flat" : level.current }), className)} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="empty-title" class={cn( "text-[15px] font-semibold tracking-[-0.2px]", className )} {...restProps}> {@render children?.()}</div><script lang="ts"> import { cn, type WithElementRef } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements";
let { ref = $bindable(null), class: className, children, ...restProps }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();</script>
<div bind:this={ref} data-slot="empty" class={cn( "flex w-full min-w-0 flex-1 flex-col items-center justify-center gap-4 rounded-[14px] border-dashed p-10 text-center text-balance", className )} {...restProps}> {@render children?.()}</div>import Root from "./empty.svelte";import Content from "./empty-content.svelte";import Description from "./empty-description.svelte";import Header from "./empty-header.svelte";import Media from "./empty-media.svelte";import Title from "./empty-title.svelte";
export { Content, Content as EmptyContent, Description, Description as EmptyDescription, Header, Header as EmptyHeader, Media, Media as EmptyMedia, Root, // Root as Empty, Title, Title as EmptyTitle,};Elevation ✦
Section titled “Elevation ✦”See the Elevation guide for the levels, roles and layered mode.
Flat by default. On EmptyMedia variant="icon", raised (+1) and floating (+2) make the media tile rise.
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { Button } from "@/components/ui/button";import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle,} from "@/components/ui/empty";import { IconPlaceholder } from "@/edmi/icon-placeholder";
const levels = [ { value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" },] as const;
export default function Demo() { return ( <div className="grid w-full max-w-3xl gap-5 sm:grid-cols-3"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Empty> <EmptyHeader> <EmptyMedia elevation={value} variant="icon"> <IconPlaceholder lucide="BriefcaseIcon" tabler="IconBriefcase" hugeicons="Briefcase01Icon" phosphor="BriefcaseIcon" remixicon="RiBriefcaseLine" /> </EmptyMedia> <EmptyTitle>No positions yet</EmptyTitle> <EmptyDescription> Your holdings will show up here. </EmptyDescription> </EmptyHeader> <EmptyContent> <Button size="sm">Explore indexes</Button> </EmptyContent> </Empty> </div> ))} </div> );}<script setup lang="ts">import { Building2Icon } from "@lucide/vue";import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<template> <div class="grid w-full max-w-3xl gap-5 sm:grid-cols-3"> <div v-for="level in levels" :key="level.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ level.label }}</p> <Empty> <EmptyHeader> <EmptyMedia variant="icon" :elevation="level.value"><Building2Icon /></EmptyMedia> <EmptyTitle>No positions yet</EmptyTitle> <EmptyDescription>Your holdings will show up here.</EmptyDescription> </EmptyHeader> </Empty> </div> </div></template><script lang="ts"> import * as Empty from "$lib/components/ui/empty"; import IconPlaceholder from "$lib/components/icon-placeholder/icon-placeholder.svelte";
const levels = [{ value: "flat", label: "Flat (0)" }, { value: "raised", label: "Raised (+1)" }, { value: "floating", label: "Floating (+2)" }] as const;</script>
<div class="grid w-full max-w-3xl gap-5 sm:grid-cols-3"> {#each levels as level (level.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{level.label}</p> <Empty.Root> <Empty.Header> <Empty.Media variant="icon" elevation={level.value}><IconPlaceholder lucide="BriefcaseIcon" tabler="IconBriefcase" hugeicons="Briefcase01Icon" phosphor="BriefcaseIcon" remixicon="RiBriefcaseLine" /></Empty.Media> <Empty.Title>No positions yet</Empty.Title> <Empty.Description>Your holdings will show up here.</Empty.Description> </Empty.Header> </Empty.Root> </div> {/each}</div>| Prop | Type | Default |
|---|---|---|
EmptyMedia variant |
default | icon |
default |
elevation |
"auto" | "sunken" | "flat" | "raised" | "floating" |
"auto" |
Outline = dashed: add border border-input to Empty (the dashed style is built in).
Parts: Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent.
✦ Edmi additions
Section titled “✦ Edmi additions”- Icon media is a 48px tile (
--card, 1px border);elevationraises it with the bevel.