Liana Pepanyan

Pricing Cards

Pricing 43

About

Pricing section for daliagents.com showing the Dali Agents packages - Starter, Agent Care, and Rescue & Migration - as part of the Dali Agents UI.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Pricing Cards to your project and fits it to your theme.

Code

How Pricing Cards is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

"use client";

import NumberFlow from "@number-flow/react";
import React from "react";

export default function PricingInteraction({
  starterMonth = 490,
  starterAnnual = 4900,
  proMonth = 1900,
  proAnnual = 1900,
}: {
  starterMonth?: number;
  starterAnnual?: number;
  proMonth?: number;
  proAnnual?: number;
}) {
  const safe = (n: number | undefined) =>
    Number.isFinite(Number(n)) ? Number(n) : 0;

  const [active, setActive] = React.useState(0);
  const [period, setPeriod] = React.useState<0 | 1>(0);
  const [starter, setStarter] = React.useState(safe(starterMonth));
  const [pro, setPro] = React.useState(safe(proMonth));

  const handleChangePlan = (i: number) => setActive(i);
  const handleChangePeriod = (i: 0 | 1) => {
    setPeriod(i);
    if (i === 0) {
      setStarter(safe(starterMonth));
      setPro(safe(proMonth));
    } else {
      setStarter(safe(starterAnnual));
      setPro(safe(proAnnual));
    }
  };

  // ---- флаг "можно анимировать" после маунта
  const [play, setPlay] = React.useState(false);
  React.useEffect(() => {
    // rAF гарантирует, что DOM уже отрисован
    const id = requestAnimationFrame(() => setPlay(true));
    return () => cancelAnimationFrame(id);
  }, []);

  // ---- минимальные частицы
  const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
  React.useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas?.getContext("2d");
    if (!canvas || !ctx) return;

    const setSize = () => {
      const rect = canvas.parentElement?.getBoundingClientRect();
      canvas.width = Math.max(1, Math.floor(rect?.width ?? window.innerWidth));
      canvas.height = Math.max(1, Math.floor(rect?.height ?? window.innerHeight));
    };
    setSize();

    type P = { x: number; y: number; v: number; o: number };
    let ps: P[] = [];
    let raf = 0;

    const make = (): P => ({
      x: Math.random() * canvas.width,
      y: Math.random() * canvas.height,
      v: Math.random() * 0.25 + 0.05,
      o: Math.random() * 0.35 + 0.15,
    });

    const init = () => {
      ps = [];
      const count = Math.floor((canvas.width * canvas.height) / 12000);
      for (let i = 0; i < count; i++) ps.push(make());
    };

    const draw = () => {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ps.forEach((p) => {
        p.y -= p.v;
        if (p.y < 0) {
          p.x = Math.random() * canvas.width;
          p.y = canvas.height + Math.random() * 40;
          p.v = Math.random() * 0.25 + 0.05;
          p.o = Math.random() * 0.35 + 0.15;
        }
        ctx.fillStyle = `rgba(250,250,250,${p.o})`;
        ctx.fillRect(p.x, p.y, 0.7, 2.2);
      });
      raf = requestAnimationFrame(draw);
    };

    const onResize = () => {
      setSize();
      init();
    };

    const ro = new ResizeObserver(onResize);
    ro.observe(canvas.parentElement || document.body);

    init();
    raf = requestAnimationFrame(draw);
    return () => {
      ro.disconnect();
      cancelAnimationFrame(raf);
    };
  }, []);

  // helper — отдаёт inline-animation значение или начальное состояние
  const anim = (name: "drawX" | "drawY" | "fadeUp", delay = 0, dur = 0.6) =>
    play
      ? ({ animation: `${name} ${dur}s ease ${delay}s forwards` } as React.CSSProperties)
      : ({} as React.CSSProperties);

  return (
    <section className="relative min-h-screen w-full overflow-hidden bg-zinc-950 text-zinc-50 grid place-items-center px-4 py-16">
      {/* Глобальные keyframes */}
      <style>{`
        @keyframes drawX { from { transform: scaleX(0); } to { transform: scaleX(1); } }
        @keyframes drawY { from { transform: scaleY(0); } to { transform: scaleY(1); } }
        @keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
      `}</style>

      {/* Виньетка */}
      <div className="pointer-events-none absolute inset-0 [background:radial-gradient(80%_60%_at_50%_12%,rgba(255,255,255,.06),transparent_60%)]" />

      {/* Линии (всё инлайном, без nth-child) */}
      <div aria-hidden className="absolute inset-0 pointer-events-none opacity-70">
        {/* Горизонтальные */}
        <div
          className="absolute left-0 right-0 bg-[#27272a]"
          style={{ top: "18%", height: 1, transform: "scaleX(0)", transformOrigin: "50% 50%", ...anim("drawX", 0.08, 0.6) }}
        />
        <div
          className="absolute left-0 right-0 bg-[#27272a]"
          style={{ top: "50%", height: 1, transform: "scaleX(0)", transformOrigin: "50% 50%", ...anim("drawX", 0.16, 0.6) }}
        />
        <div
          className="absolute left-0 right-0 bg-[#27272a]"
          style={{ top: "82%", height: 1, transform: "scaleX(0)", transformOrigin: "50% 50%", ...anim("drawX", 0.24, 0.6) }}
        />
        {/* Вертикальные */}
        <div
          className="absolute top-0 bottom-0 bg-[#27272a]"
          style={{ left: "22%", width: 1, transform: "scaleY(0)", transformOrigin: "50% 0%", ...anim("drawY", 0.20, 0.7) }}
        />
        <div
          className="absolute top-0 bottom-0 bg-[#27272a]"
          style={{ left: "50%", width: 1, transform: "scaleY(0)", transformOrigin: "50% 0%", ...anim("drawY", 0.28, 0.7) }}
        />
        <div
          className="absolute top-0 bottom-0 bg-[#27272a]"
          style={{ left: "78%", width: 1, transform: "scaleY(0)", transformOrigin: "50% 0%", ...anim("drawY", 0.36, 0.7) }}
        />
      </div>

      {/* Частицы */}
      <canvas
        ref={canvasRef}
        className="absolute inset-0 h-full w-full opacity-50 mix-blend-screen pointer-events-none"
      />

      {/* Карточка (fadeUp инлайн) */}
      <div
        className="relative w-full max-w-md rounded-[28px] border border-zinc-800 bg-zinc-900/70 backdrop-blur supports-[backdrop-filter]:bg-zinc-900/60 shadow-xl p-4"
        style={{ opacity: 0, transform: "translateY(12px)", ...anim("fadeUp", 0.32, 0.6) }}
      >
        {/* Переключатель периода */}
        <div className="rounded-full relative w-full bg-zinc-800/50 p-1.5 flex items-center">
          <button
            className="font-medium rounded-full w-full p-1.5 text-zinc-200 z-20"
            onClick={() => handleChangePeriod(0)}
            aria-pressed={period === 0}
          >
            Monthly
          </button>
          <button
            className="font-medium rounded-full w-full p-1.5 text-zinc-200 z-20"
            onClick={() => handleChangePeriod(1)}
            aria-pressed={period === 1}
          >
            Yearly
          </button>
          <div
            className="p-1.5 flex items-center justify-center absolute inset-0 w-1/2 z-10"
            style={{
              transform: `translateX(${period * 100}%)`,
              transition: "transform 0.3s",
            }}
          >
            <div className="bg-zinc-900 rounded-full w-full h-full border border-zinc-700 shadow-sm" />
          </div>
        </div>

        {/* Список планов */}
        <div className="w-full relative mt-3 flex flex-col items-center justify-center gap-3">
          {/* Starter */}
          <div
            className="w-full flex justify-between cursor-pointer border border-zinc-800/80 hover:border-zinc-700 transition-colors p-4 rounded-2xl bg-zinc-900/50"
            onClick={() => handleChangePlan(0)}
          >
            <div className="flex flex-col items-start">
              <p className="font-semibold text-xl text-zinc-50">Starter</p>
              <p className="text-zinc-400 text-sm">
                <span className="text-zinc-100 font-medium">Fixed quote</span>, one agent
              </p>
            </div>
            <RadioDot active={active === 0} />
          </div>

          {/* Agent Care */}
          <div
            className="w-full flex justify-between cursor-pointer border border-zinc-800/80 hover:border-zinc-700 transition-colors p-4 rounded-2xl bg-zinc-900/50"
            onClick={() => handleChangePlan(1)}
          >
            <div className="flex flex-col items-start">
              <p className="font-semibold text-xl flex items-center gap-2 text-zinc-50">
                Agent Care
                <span className="py-1 px-2 rounded-lg bg-yellow-100/20 text-yellow-200 text-xs border border-yellow-200/30">
                  Retainer
                </span>
              </p>
              <p className="text-zinc-400 text-sm flex items-baseline gap-1">
                <span className="text-zinc-100 font-medium">$</span>
                <NumberFlow className="text-zinc-100 font-medium" value={starter} />
                <span>/{period === 0 ? "month" : "year"}</span>
              </p>
            </div>
            <RadioDot active={active === 1} />
          </div>

          {/* Rescue & Migration */}
          <div
            className="w-full flex justify-between cursor-pointer border border-zinc-800/80 hover:border-zinc-700 transition-colors p-4 rounded-2xl bg-zinc-900/50"
            onClick={() => handleChangePlan(2)}
          >
            <div className="flex flex-col items-start">
              <p className="font-semibold text-xl text-zinc-50">Rescue & Migration</p>
              <p className="text-zinc-400 text-sm flex items-baseline gap-1">
                <span className="text-zinc-100 font-medium">from $</span>
                <NumberFlow className="text-zinc-100 font-medium" value={pro} />
                <span>, fixed scope</span>
              </p>
            </div>
            <RadioDot active={active === 2} />
          </div>

          {/* Подсветка выбранного ряда */}
          <div
            className="pointer-events-none w-full h-[88px] absolute top-0 rounded-2xl border-2 border-zinc-100/80"
            style={{
              transform: `translateY(${active * 88 + 12 * active}px)`,
              transition: "transform 0.3s",
            }}
          />
        </div>

        <button className="mt-4 rounded-full bg-zinc-100 text-lg text-zinc-900 w-full p-3 active:scale-95 transition-transform duration-300 hover:bg-zinc-200">
          Start with one agent
        </button>

        <a
          href="https://daliagents.com"
          target="_blank"
          rel="noreferrer"
          className="mt-3 block text-center text-[10px] tracking-wide text-zinc-600 transition-colors hover:text-zinc-400"
        >
          daliagents.com
        </a>
      </div>
    </section>
  );
}

function RadioDot({ active }: { active: boolean }) {
  return (
    <div
      className="border-2 size-6 rounded-full mt-0.5 p-1 flex items-center justify-center"
      style={{ borderColor: active ? "#fff" : "#3f3f46", transition: "border-color 0.3s" }}
    >
      <div
        className="size-3 bg-zinc-100 rounded-full"
        style={{ opacity: active ? 1 : 0, transition: "opacity 0.3s" }}
      />
    </div>
  );
}

Dependencies

More by Liana Pepanyan

View all
  • CTA with Text Marquee

    CTA with Text Marquee

    1.1K
  • Dock

    Dock

    421
  • Hero Minimalism

    Hero Minimalism

    413
  • CTA with Marquee

    CTA with Marquee

    355
  • Dark Grid

    Dark Grid

    203
  • Processing Card

    Processing Card

    197
  • Login / Signup

    Login / Signup

    194
  • Blueprint Shader Grid

    Blueprint Shader Grid

    136

More in Pricing

View all
  • Pricing Section

    Pricing Section

    582
  • Pricing Section

    Pricing Section

    173
  • Pricing Card

    Pricing Card

    365
  • pricing section

    pricing section

    158
  • Pricing Cards

    Pricing Cards

    86
  • Colorful-Pricing

    Colorful-Pricing

    85
  • Glowing Pricing Tiers

    Glowing Pricing Tiers

    45
  • RuixenPricing05

    RuixenPricing05

    102