Menu

Change language

GitHub repo

Conversion Funnel

blockanalytics

Horizontal conversion funnel with count strip above the SmoothFunnel chart and a stage-name + retention-from-previous footer below. Works for hiring (applied → hired), e-commerce (sessions → purchases), onboarding (signed up → activated). 3-6 stages. Theme-aware via OKLCH chart tokens.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/conversion-funnel.json
Manifest:https://uipkge.dev/r/svelte/conversion-funnel.jsonInstalls to:src/lib/components/blocks/conversion-funnel/

Variants

Loading interactive previews…

Theming

CSS custom properties referenced in this item. Override any of them in your :root or per-element to retheme.

--chart-1--chart-2--chart-3--chart-4--chart-5

Files installed (1)

  • src/lib/components/blocks/conversion-funnel/ConversionFunnel.svelte3.2 kB
    <!--
      Conversion funnel: SmoothFunnel chart with count strip above and
      stage-name + retention-from-previous strip below.
    
      Works for any drop-off flow with 3-6 stages: hiring (applied ->
      hired), e-commerce (sessions -> purchases), onboarding (signed up
      -> activated), etc. Reads as a single horizontal "flow":
    
        72K        38.2K       16.8K       5.6K
        ╔══════╗╔════════╗╔══════════╗╔══════════╗
        ║ 100% ║║  53%   ║║   23%    ║║    8%    ║
        ╚══════╝╚════════╝╚══════════╝╚══════════╝
        Views      Cart        Checkout    Purchase
                   -> 53%       -> 44%      -> 33%
    
      The middle pill (inside each band) shows the share OF THE TOP
      stage (100%, 53%, 23%, ...). The bottom retention number shows the
      stage-to-stage conversion -- usually the more actionable signal.
    -->
    <script lang="ts" module>
      export interface FunnelBlockStage {
        name: string
        value: number
      }
    
      export interface ConversionFunnelProps {
        data: FunnelBlockStage[]
        /** SmoothFunnel container height. Defaults to 180. */
        height?: number | string
        /** Override colour palette. Defaults to the registry's chart-1..N
         *  OKLCH tokens via CSS variables (so light/dark + theme-customizer
         *  flips ripple through without any JS). Pass a literal hex array to
         *  override. */
        colors?: string[]
        /** Compact number formatter for the count strip (default toLocaleString). */
        format?: (n: number) => string
        /** Hide the retention-from-previous footer on each non-first stage. */
        hideRetention?: boolean
        class?: string
      }
    </script>
    
    <script lang="ts">
      import { cn } from '$lib/utils'
      import { SmoothFunnel } from '$lib/components/ui/smooth-funnel'
    
      const DEFAULT_PALETTE = [
        'var(--chart-1)',
        'var(--chart-2)',
        'var(--chart-3)',
        'var(--chart-4)',
        'var(--chart-5)',
        'var(--chart-1)',
      ]
    
      let {
        data,
        height = 180,
        colors,
        format,
        hideRetention = false,
        class: className,
      }: ConversionFunnelProps = $props()
    
      const palette = $derived(colors ?? DEFAULT_PALETTE)
      const fmt = (v: number) => (format ? format(v) : v.toLocaleString())
      function retention(i: number) {
        if (i === 0) return null
        const prev = data[i - 1]?.value ?? 1
        return Math.round(((data[i]?.value ?? 0) / prev) * 100)
      }
    </script>
    
    <div data-uipkge data-slot="conversion-funnel" class={cn('space-y-2', className)}>
      <!-- Top: per-stage count -->
      <div class="grid px-2 text-center" style:grid-template-columns={`repeat(${data.length}, minmax(0, 1fr))`}>
        {#each data as stage (stage.name)}
          <div class="text-foreground text-base font-bold tabular-nums">
            {fmt(stage.value)}
          </div>
        {/each}
      </div>
    
      <!-- Funnel SVG -->
      <SmoothFunnel {data} {height} colors={palette} />
    
      <!-- Bottom: stage name + retention from previous -->
      <div class="grid px-2 text-center" style:grid-template-columns={`repeat(${data.length}, minmax(0, 1fr))`}>
        {#each data as stage, i (stage.name)}
          <div class="space-y-0.5">
            <p class="text-muted-foreground text-xs">{stage.name}</p>
            {#if !hideRetention && retention(i) !== null}
              <p class="text-info text-xs font-semibold tabular-nums">
                → {retention(i)}% retained
              </p>
            {/if}
          </div>
        {/each}
      </div>
    </div>
    

Raw manifest:https://uipkge.dev/r/svelte/conversion-funnel.json