Skip to content
edmi-ui

Empty

What to show when there is nothing yet. Icon media, title, description and actions; dashed outline.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
No positions yet
Join an index and your holdings will show up here.
Drop your weights file
CSV with symbol and weight columns.
No positions yet
Join an index and your holdings will show up here.
Drop your weights file
CSV with symbol and weight columns.
No positions yet
Join an index and your holdings will show up here.
Drop your weights file
CSV with symbol and weight columns.
Terminal window
npx shadcn@latest add @edmi-ui/empty
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/empty
Terminal window
yarn dlx shadcn@latest add @edmi-ui/empty
Terminal window
bunx shadcn@latest add @edmi-ui/empty

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/empty
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/empty
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/empty
Terminal window
bunx shadcn-vue@latest add @edmi-ui/empty

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/empty.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/empty.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/empty.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/empty.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

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>
registry/ui/empty.tsx
"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,
};
registry/ui/empty/Empty.vue
<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>
registry/ui/empty/EmptyContent.vue
<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>
registry/ui/empty/EmptyDescription.vue
<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>
registry/ui/empty/EmptyHeader.vue
<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>
registry/ui/empty/EmptyMedia.vue
<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>
registry/ui/empty/EmptyTitle.vue
<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>
registry/ui/empty/index.ts
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>;
src/lib/registry/ui/empty/empty-content.svelte
<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>
src/lib/registry/ui/empty/empty-description.svelte
<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>
src/lib/registry/ui/empty/empty-header.svelte
<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>
src/lib/registry/ui/empty/empty-media.svelte
<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>
src/lib/registry/ui/empty/empty-title.svelte
<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>
src/lib/registry/ui/empty/empty.svelte
<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>
src/lib/registry/ui/empty/index.ts
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,
};

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.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

No positions yet
Your holdings will show up here.

Raised (+1)

No positions yet
Your holdings will show up here.

Floating (+2)

No positions yet
Your holdings will show up here.

Flat (0)

No positions yet
Your holdings will show up here.

Raised (+1)

No positions yet
Your holdings will show up here.

Floating (+2)

No positions yet
Your holdings will show up here.

Flat (0)

No positions yet
Your holdings will show up here.

Raised (+1)

No positions yet
Your holdings will show up here.

Floating (+2)

No positions yet
Your holdings will show up here.
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.

  • Icon media is a 48px tile (--card, 1px border); elevation raises it with the bevel.