Skip to content
edmi-ui

Pricing Plan

Plan card with title, price, call to action and a check list.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks
Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks
Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks
Terminal window
npx shadcn@latest add @edmi-ui/pricing-plan
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/pricing-plan
Terminal window
yarn dlx shadcn@latest add @edmi-ui/pricing-plan
Terminal window
bunx shadcn@latest add @edmi-ui/pricing-plan

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/pricing-plan
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/pricing-plan
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/pricing-plan
Terminal window
bunx shadcn-vue@latest add @edmi-ui/pricing-plan

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/pricing-plan.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pricing-plan.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pricing-plan.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pricing-plan.json

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

import { PricingPlan } from "@/components/pricing-plan/pricing-plan";
import { Button } from "@/components/ui/button";
<PricingPlan
className="w-[300px] max-w-full"
name="Creator"
tagline="Launch your own index"
price="1% fee to you"
priceNote="Earned on every holder’s share."
action={<Button size="lg">Launch an index</Button>}
features={[
"Custom weights and mandate",
"Pre-IPO sleeve",
"Share cards and Blinks",
]}
/>
<script setup lang="ts">
import { Button } from "@/components/ui/button";
import { PricingPlan } from "@/components/ui/pricing-plan";
</script>
<template>
<PricingPlan
class="w-[300px] max-w-full"
name="Creator"
tagline="Launch your own index"
price="1% fee to you"
price-note="Earned on every holder’s share."
:features="['Custom weights and mandate', 'Pre-IPO sleeve', 'Share cards and Blinks']"
>
<template #action><Button size="lg">Launch an index</Button></template>
</PricingPlan>
</template>
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { PricingPlan } from "$lib/components/ui/pricing-plan";
</script>
<PricingPlan
class="w-[300px] max-w-full"
name="Creator"
tagline="Launch your own index"
price="1% fee to you"
priceNote="Earned on every holder’s share."
features={["Custom weights and mandate", "Pre-IPO sleeve", "Share cards and Blinks"]}
>
{#snippet action()}
<Button size="lg">Launch an index</Button>
{/snippet}
</PricingPlan>
registry/blocks/pricing-plan/pricing-plan.tsx
import { cn } from "cn";
import type * as React from "react";
import { CheckIcon } from "@phosphor-icons/react";
import { Card } from "@/registry/edmi/ui/card";
import type { Elevation } from "@/registry/edmi/ui/elevation";
import { Separator } from "@/registry/edmi/ui/separator";
type PricingPlanProps = Omit<React.ComponentProps<typeof Card>, "title"> & {
name: React.ReactNode;
tagline?: React.ReactNode;
/** Headline price, e.g. "1% fee to you". */
price?: React.ReactNode;
/** Small note under the price. */
priceNote?: React.ReactNode;
/** Call to action, usually a full-width `Button`. */
action?: React.ReactNode;
features?: React.ReactNode[];
/** ✦ Lead row above the features, e.g. "Included:" or "Everything in Holder, plus:". */
featuresLead?: React.ReactNode;
/** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
};
function PricingPlan({
className,
name,
tagline,
price,
priceNote,
action,
features,
featuresLead,
elevation,
...props
}: PricingPlanProps) {
return (
<Card
data-slot="pricing-plan"
elevation={elevation}
className={cn("gap-0 p-6", className)}
{...props}
>
<div className="text-[22px] font-semibold">{name}</div>
{tagline ? (
<div className="mt-1 text-[13.5px] text-muted-foreground">
{tagline}
</div>
) : null}
{price ? (
<div className="mt-[22px] text-[22px] font-semibold">{price}</div>
) : null}
{priceNote ? (
<div className="mt-1 text-[12.5px] text-muted-foreground">
{priceNote}
</div>
) : null}
{action ? (
<div className="mt-[18px] [&>[data-slot=button]]:w-full">{action}</div>
) : null}
{features?.length ? (
<>
<Separator className="my-[18px]" />
{featuresLead ? (
<div
data-slot="pricing-plan-lead"
className="mb-3.5 text-[15px] font-semibold"
>
{featuresLead}
</div>
) : null}
<ul className="flex flex-col gap-2.5 text-[13.5px]">
{features.map((f, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: static list
<li key={i} className="flex items-center gap-2.5">
<CheckIcon
className="size-3.5 text-muted-foreground"
/>
{f}
</li>
))}
</ul>
</>
) : null}
</Card>
);
}
export { PricingPlan };
registry/ui/pricing-plan/PricingPlan.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Check } from '@lucide/vue'
import { Card } from '@/registry/edmi/ui/card'
import type { Elevation } from '@/registry/edmi/ui/elevation'
import { Separator } from '@/registry/edmi/ui/separator'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
/** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2 */
elevation?: Elevation
class?: HTMLAttributes['class']
name?: string
tagline?: string
/** Headline price, e.g. "1% fee to you". */
price?: string
/** Small note under the price. */
priceNote?: string
features?: string[]
/** ✦ Lead row above the features, e.g. "Included:" or "Everything in Holder, plus:". */
featuresLead?: string
}>()
// Slots `name`, `tagline`, `price`, `price-note` (string props are the fallback).
// Slot `action` = call to action, usually a full-width `Button`.
// ✦ Slot `features-lead` = rich lead row (overrides `featuresLead`).
// ✦ Scoped slot `feature` ({ feature, index }) = rich content for a feature row (default: the text).
</script>
<template>
<Card :elevation="elevation" data-slot="pricing-plan" :class="cn('gap-0 p-6', props.class)">
<div class="text-[22px] font-semibold"><slot name="name">{{ name }}</slot></div>
<div v-if="tagline || $slots.tagline" class="mt-1 text-[13.5px] text-muted-foreground"><slot name="tagline">{{ tagline }}</slot></div>
<div v-if="price || $slots.price" class="mt-[22px] text-[22px] font-semibold"><slot name="price">{{ price }}</slot></div>
<div v-if="priceNote || $slots['price-note']" class="mt-1 text-[12.5px] text-muted-foreground"><slot name="price-note">{{ priceNote }}</slot></div>
<div v-if="$slots.action" class="mt-[18px] [&>[data-slot=button]]:w-full">
<slot name="action" />
</div>
<template v-if="features?.length">
<Separator class="my-[18px]" />
<div v-if="featuresLead || $slots['features-lead']" data-slot="pricing-plan-lead" class="mb-3.5 text-[15px] font-semibold">
<slot name="features-lead">
{{ featuresLead }}
</slot>
</div>
<ul class="flex flex-col gap-2.5 text-[13.5px]">
<li v-for="(f, i) in features" :key="i" class="flex items-center gap-2.5">
<Check class="size-3.5 text-muted-foreground" />
<slot name="feature" :feature="f" :index="i">
{{ f }}
</slot>
</li>
</ul>
</template>
</Card>
</template>
registry/ui/pricing-plan/index.ts
export { default as PricingPlan } from "./PricingPlan.vue";
src/lib/registry/ui/pricing-plan/index.ts
import Root from "./pricing-plan.svelte";
export {
Root,
//
Root as PricingPlan,
};
src/lib/registry/ui/pricing-plan/pricing-plan.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Card } from "$lib/registry/ui/card/index.js";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
import { Separator } from "$lib/registry/ui/separator/index.js";
import CheckIcon from "phosphor-svelte/lib/Check";
import type { ComponentProps, Snippet } from "svelte";
let {
ref = $bindable(null),
class: className,
name,
tagline,
price,
priceNote,
action,
features,
featuresLead,
feature,
elevation = "auto",
...restProps
}: Omit<ComponentProps<typeof Card>, "title" | "children"> & {
name: string | Snippet;
tagline?: string | Snippet;
/** Headline price, e.g. "1% fee to you". */
price?: string | Snippet;
/** Small note under the price. */
priceNote?: string | Snippet;
/** Call to action, usually a full-width `Button`. */
action?: Snippet;
features?: (string | Snippet)[];
/** ✦ Lead row above the features, e.g. "Included:" or "Everything in Holder, plus:". */
featuresLead?: string | Snippet;
/** ✦ Rich content for a feature row (default: the text). */
feature?: Snippet<[string | Snippet, number]>;
/** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
} = $props();
</script>
<Card bind:ref {elevation} data-slot="pricing-plan" class={cn("gap-0 p-6", className)} {...restProps}>
<div class="text-[22px] font-semibold">{#if typeof name === "function"}{@render name()}{:else}{name}{/if}</div>
{#if tagline}<div class="mt-1 text-[13.5px] text-muted-foreground">{#if typeof tagline === "function"}{@render tagline()}{:else}{tagline}{/if}</div>{/if}
{#if price}<div class="mt-[22px] text-[22px] font-semibold">{#if typeof price === "function"}{@render price()}{:else}{price}{/if}</div>{/if}
{#if priceNote}<div class="mt-1 text-[12.5px] text-muted-foreground">{#if typeof priceNote === "function"}{@render priceNote()}{:else}{priceNote}{/if}</div>{/if}
{#if action}
<div class="mt-[18px] [&>[data-slot=button]]:w-full">{@render action()}</div>
{/if}
{#if features?.length}
<Separator class="my-[18px]" />
{#if featuresLead}
<div data-slot="pricing-plan-lead" class="mb-3.5 text-[15px] font-semibold">
{#if typeof featuresLead === "function"}{@render featuresLead()}{:else}{featuresLead}{/if}
</div>
{/if}
<ul class="flex flex-col gap-2.5 text-[13.5px]">
{#each features as f, i (i)}
<li class="flex items-center gap-2.5">
<CheckIcon
class="size-3.5 text-muted-foreground"
/>
{#if feature}{@render feature(f, i)}{:else if typeof f === "function"}{@render f()}{:else}{f}{/if}
</li>
{/each}
</ul>
{/if}
</Card>

See the Elevation guide for the levels, roles and layered mode.

Flat by default. elevation is forwarded to the card: sunken (-1) a soft inset well, raised (+1) the bevel, floating (+2) the bevel plus one soft drop. In a layered ElevationProvider the card resolves to its role default (raised).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks

Raised (+1)

Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks

Floating (+2)

Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks

Flat (0)

Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks

Raised (+1)

Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks

Floating (+2)

Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks

Flat (0)

Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks

Raised (+1)

Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks

Floating (+2)

Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
  • Custom weights and mandate
  • Pre-IPO sleeve
  • Share cards and Blinks
Part Prop Type Default
PricingPlan name React ReactNode; Vue slot name (string prop as fallback); Svelte string | Snippet required
tagline, price, priceNote React ReactNode; Vue slots tagline, price, price-note (string props as fallback); Svelte string | Snippet none
action React ReactNode; Vue slot action; Svelte action snippet. Usually a Button; buttons there stretch full width none
features React ReactNode[]; Vue string[]; Svelte (string | Snippet)[] (rich rows: see feature below) none
feature ✦ Vue scoped slot feature ({ feature, index }); Svelte snippet feature(text, index). React renders any node directly from features the text
featuresLead ✦ Lead row above the features. React ReactNode; Vue string or slot features-lead; Svelte string | Snippet none
elevation ✦ "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
StackBlitz ↗StackBlitz ↗StackBlitz ↗
Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
Everything in Holder, plus:
  • Custom weights and mandate
  • Pre-IPO sleeve New
  • Share cards and Blinks
Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
Everything in Holder, plus:
  • Custom weights and mandate
  • Pre-IPO sleeve
    New
  • Share cards and Blinks
Creator
Launch your own index
1% fee to you
Earned on every holder’s share.
Everything in Holder, plus:
  • Custom weights and mandate
  • Pre-IPO sleeve New
  • Share cards and Blinks
  • Whole block is an Edmi addition (DESIGN §5 Patterns, board 12). Composed from: card, separator.
  • Install: shadcn add @edmi-ui/pricing-plan.