Pricing Plan
Plan card with title, price, call to action and a check list.
import { PricingPlan } from "@/components/pricing-plan/pricing-plan";import { Button } from "@/components/ui/button";
export default function Demo() { return ( <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>Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/pricing-planpnpm dlx shadcn@latest add @edmi-ui/pricing-planyarn dlx shadcn@latest add @edmi-ui/pricing-planbunx shadcn@latest add @edmi-ui/pricing-planPick 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:
card,separator,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/pricing-planpnpm dlx shadcn-vue@latest add @edmi-ui/pricing-planyarn dlx shadcn-vue@latest add @edmi-ui/pricing-planbunx shadcn-vue@latest add @edmi-ui/pricing-planPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install @lucide/vueTerminal window pnpm add @lucide/vueTerminal window yarn add @lucide/vueTerminal window bun add @lucide/vueAdd the Edmi components this one builds on:
card,separator,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/pricing-plan.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pricing-plan.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pricing-plan.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/pricing-plan.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:
card,separator,elevation.Copy the code from Component source into your project and update the import paths to match your aliases.
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>Component source
Section titled “Component source”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 };<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>export { default as PricingPlan } from "./PricingPlan.vue";import Root from "./pricing-plan.svelte";
export { Root, // Root as PricingPlan,};<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>Elevation ✦
Section titled “Elevation ✦”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).
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
Flat (0)
Raised (+1)
Floating (+2)
import { PricingPlan } from "@/components/pricing-plan/pricing-plan";import { Button } from "@/components/ui/button";import type { Elevation } from "@/components/ui/elevation";
function Sample({ elevation }: { elevation: Elevation }) { return ( <PricingPlan elevation={elevation} 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", ]} /> );}
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="flex flex-col gap-6"> {levels.map(({ value, label }) => ( <div key={value} className="flex flex-col gap-2"> <p className="text-xs font-medium text-muted-foreground">{label}</p> <Sample elevation={value} /> </div> ))} </div> );}<script setup lang="ts">import { Button } from "@/components/ui/button";import { PricingPlan } from "@/components/ui/pricing-plan";
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const,];</script>
<template> <div class="flex flex-col gap-6"> <div v-for="l in levels" :key="l.value" class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{{ l.label }}</p> <PricingPlan :elevation="l.value" 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> </div> </div></template><script lang="ts"> import { Button } from "$lib/components/ui/button"; import { PricingPlan } from "$lib/components/ui/pricing-plan";
const levels = [ { value: "flat", label: "Flat (0)" } as const, { value: "raised", label: "Raised (+1)" } as const, { value: "floating", label: "Floating (+2)" } as const, ];</script>
<div class="flex flex-col gap-6"> {#each levels as l (l.value)} <div class="flex flex-col gap-2"> <p class="text-xs font-medium text-muted-foreground">{l.label}</p><PricingPlan elevation={l.value} 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" elevation={l.value}>Launch an index</Button> {/snippet}</PricingPlan> </div> {/each}</div>| 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" |
Lead row and rich features ✦
Section titled “Lead row and rich features ✦”import { PricingPlan } from "@/components/pricing-plan/pricing-plan";import { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";
export default function Demo() { return ( <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>} featuresLead="Everything in Holder, plus:" features={[ "Custom weights and mandate", <span key="ipo" className="flex items-center gap-2"> Pre-IPO sleeve <Badge variant="secondary">New</Badge> </span>, "Share cards and Blinks", ]} /> );}<script setup lang="ts">import { Badge } from "@/components/ui/badge";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-lead="Everything in Holder, plus:" :features="['Custom weights and mandate', 'Pre-IPO sleeve', 'Share cards and Blinks']" > <template #action><Button size="lg">Launch an index</Button></template> <template #feature="{ feature }"> <span class="flex items-center gap-2"> {{ feature }} <Badge v-if="feature === 'Pre-IPO sleeve'" variant="secondary">New</Badge> </span> </template> </PricingPlan></template><script lang="ts"> import { Badge } from "$lib/components/ui/badge"; 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." featuresLead="Everything in Holder, plus:" features={["Custom weights and mandate", "Pre-IPO sleeve", "Share cards and Blinks"]}> {#snippet action()} <Button size="lg">Launch an index</Button> {/snippet} {#snippet feature(text)} <span class="flex items-center gap-2"> {text} {#if text === "Pre-IPO sleeve"}<Badge variant="secondary">New</Badge>{/if} </span> {/snippet}</PricingPlan>✦ Edmi additions
Section titled “✦ Edmi additions”- Whole block is an Edmi addition (DESIGN §5 Patterns, board 12). Composed from: card, separator.
- Install:
shadcn add @edmi-ui/pricing-plan.