Features & services
Services Pricing
What a service includes beside a pricing card whose plan switch swaps the list, price and note.
Layout and motion from the Nitip template — see it in context"use client";
import * as React from "react";
import { SegmentedControl } from "@/components/ui/segmented-control";
import { SpringButton } from "@/components/ui/spring-button";
import { cn } from "@/lib/utils";
/*
* Services Pricing — what a service includes (tools, what we offer, key benefits) beside a pricing card whose plan
* switch swaps the included list, the price and the note.
* Source: marcframe/nitip → components/sections/ServiceContentSection.tsx (+ design-system PricingCard, TextPoint,
* HighlightList).
*
* Layout kept from Framer: below 1024px the card follows the text; from 1024px the text fills the row and the card is
* 400px wide, 96px from the text and as tall as it, its content at the top.
* Motion kept: the plan switch's fill slides to the chosen plan (segmented-control; its spring is bounce 0.15, 0.45s
* instead of Nitip's bounce 0, 0.4s). The switch only shows with more than one plan.
* The section heading is for screen readers (Nitip's page opens straight on "Tools"); the three lists are h3.
*/
export type PricingItem = { highlight?: string; text: string };
export type PricingPlan = { id: string; label: string; price: string; note: string; included: string[] };
export type ServicesPricingProps = {
/** Section heading, visually hidden. */
title?: string;
tools?: { label: string; href?: string }[];
offer?: PricingItem[];
benefits?: PricingItem[];
/** Package name at the top of the card. */
name?: string;
plans?: PricingPlan[];
action?: { label: string; href: string };
labels?: { tools: string; offer: string; benefits: string; included: string; plans: string };
className?: string;
};
const defaults = {
title: "Paid advertising: what's included",
labels: { tools: "Tools", offer: "What we offer", benefits: "Key benefits", included: "What's included", plans: "Billing" },
tools: [
{ label: "Google Ads", href: "https://ads.google.com" },
{ label: "Meta Ads Manager", href: "https://www.facebook.com/business/tools/ads-manager" },
{ label: "LinkedIn Campaign Manager", href: "https://business.linkedin.com/marketing-solutions" },
{ label: "Google Analytics", href: "https://analytics.google.com" },
],
offer: [
{ highlight: "Campaign strategy", text: ": audiences, budgets and goals set from your margins, not guesswork." },
{ highlight: "Ad creative", text: ": copy and visuals made for each channel and tested against each other." },
{ highlight: "Weekly optimization", text: ": bids, keywords and placements tuned every week." },
{ highlight: "Clear reporting", text: ": one dashboard and a monthly call in plain numbers." },
],
benefits: [
{ highlight: "Traffic from day one", text: ": qualified visitors while search and social grow." },
{ highlight: "Spend you can defend", text: ": every campaign tied to cost per lead and return." },
{ highlight: "No lock-in", text: ": your accounts, your data, cancel any month." },
],
name: "Paid advertising package",
plans: [
{
id: "monthly",
label: "Monthly",
price: "$1,800",
note: "per month, plus ad spend",
included: ["Two channels", "Up to 12 ad variations", "Weekly optimization", "Monthly report and call"],
},
{
id: "quarterly",
label: "Quarterly",
price: "$4,900",
note: "per quarter, plus ad spend — save 9%",
included: ["Three channels", "Up to 30 ad variations", "Weekly optimization", "Quarterly strategy review"],
},
],
action: { label: "Book an intro call", href: "/contact" },
};
export function ServicesPricing({
title = defaults.title,
tools = defaults.tools,
offer = defaults.offer,
benefits = defaults.benefits,
name = defaults.name,
plans = defaults.plans,
action = defaults.action,
labels = defaults.labels,
className,
}: ServicesPricingProps) {
const [planId, setPlanId] = React.useState(plans[0]?.id);
const plan = plans.find((item) => item.id === planId) ?? plans[0];
return (
<section className={cn("py-(--section-y)", className)}>
<h2 className="sr-only">{title}</h2>
<div className="mx-auto flex max-w-(--container) flex-col gap-12 px-(--gutter) lg:flex-row lg:gap-24">
<div className="flex min-w-0 flex-col gap-12 lg:flex-1">
{tools.length > 0 && (
<div className="flex flex-col gap-2.5">
<h3 className="font-heading text-h3 font-semibold tracking-tight">{labels.tools}</h3>
<hr className="border-border" />
<ul>
{tools.map((tool) => (
<li key={tool.label}>
{tool.href ? (
<a
href={tool.href}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-5 border-b border-border py-2.5 transition-colors hover:text-foreground/80 hover:underline hover:underline-offset-4"
>
<span className="flex-1">{tool.label}</span>
<span aria-hidden className="size-2 rounded-full bg-foreground" />
</a>
) : (
<span className="flex items-center gap-5 border-b border-border py-2.5">
<span className="flex-1">{tool.label}</span>
<span aria-hidden className="size-2 rounded-full bg-foreground" />
</span>
)}
</li>
))}
</ul>
</div>
)}
{[
{ heading: labels.offer, items: offer },
{ heading: labels.benefits, items: benefits },
].map(
(group) =>
group.items.length > 0 && (
<div key={group.heading} className="flex flex-col gap-5">
<h3 className="font-heading text-h3 font-semibold tracking-tight">{group.heading}</h3>
<ul className="list-disc space-y-2 pl-5 text-muted-foreground marker:text-foreground">
{group.items.map((item) => (
<li key={(item.highlight ?? "") + item.text}>
{item.highlight && <strong className="font-semibold text-foreground">{item.highlight}</strong>}
{item.text}
</li>
))}
</ul>
</div>
),
)}
</div>
{plan && (
<div className="flex w-full flex-col gap-8 rounded-xl border border-border bg-card p-6 text-card-foreground lg:w-100 lg:shrink-0">
<div className="flex flex-col items-start gap-5">
{plans.length > 1 && (
<SegmentedControl
label={labels.plans}
size="sm"
value={plan.id}
onValueChange={setPlanId}
options={plans.map((item) => ({ value: item.id, label: item.label }))}
/>
)}
<h3 className="font-heading text-h3 font-semibold tracking-tight">{name}</h3>
</div>
<div className="flex flex-col gap-4">
<p className="font-medium">{labels.included}</p>
<ul className="flex flex-col gap-2.5" aria-live="polite">
{plan.included.map((item) => (
<li key={item} className="flex items-start gap-2.5 text-muted-foreground">
<svg
viewBox="0 0 16 16"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
className="mt-1 size-4 shrink-0 text-foreground"
>
<path d="M3 8.5l3 3 7-7" />
</svg>
<span className="min-w-0 flex-1">{item}</span>
</li>
))}
</ul>
</div>
<div className="flex flex-col items-start gap-5">
<div>
<p className="font-heading text-h2 font-semibold tracking-tight">{plan.price}</p>
<p className="text-sm text-muted-foreground">{plan.note}</p>
</div>
<SpringButton href={action.href}>{action.label}</SpringButton>
</div>
</div>
)}
</div>
</section>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/services-pricing.json1. Add the Framebrick UI components it uses (they bring Motion).
npx shadcn@latest add https://framebrick.co/r/segmented-control.json https://framebrick.co/r/spring-button.json2. Add the block tokens to
app/globals.cssonce (theming).npx shadcn@latest add https://framebrick.co/r/blocks/tokens.json3. Copy the block into
components/services-pricing.tsx."use client"; import * as React from "react"; import { SegmentedControl } from "@/components/ui/segmented-control"; import { SpringButton } from "@/components/ui/spring-button"; import { cn } from "@/lib/utils"; /* * Services Pricing — what a service includes (tools, what we offer, key benefits) beside a pricing card whose plan * switch swaps the included list, the price and the note. * Source: marcframe/nitip → components/sections/ServiceContentSection.tsx (+ design-system PricingCard, TextPoint, * HighlightList). * * Layout kept from Framer: below 1024px the card follows the text; from 1024px the text fills the row and the card is * 400px wide, 96px from the text and as tall as it, its content at the top. * Motion kept: the plan switch's fill slides to the chosen plan (segmented-control; its spring is bounce 0.15, 0.45s * instead of Nitip's bounce 0, 0.4s). The switch only shows with more than one plan. * The section heading is for screen readers (Nitip's page opens straight on "Tools"); the three lists are h3. */ export type PricingItem = { highlight?: string; text: string }; export type PricingPlan = { id: string; label: string; price: string; note: string; included: string[] }; export type ServicesPricingProps = { /** Section heading, visually hidden. */ title?: string; tools?: { label: string; href?: string }[]; offer?: PricingItem[]; benefits?: PricingItem[]; /** Package name at the top of the card. */ name?: string; plans?: PricingPlan[]; action?: { label: string; href: string }; labels?: { tools: string; offer: string; benefits: string; included: string; plans: string }; className?: string; }; const defaults = { title: "Paid advertising: what's included", labels: { tools: "Tools", offer: "What we offer", benefits: "Key benefits", included: "What's included", plans: "Billing" }, tools: [ { label: "Google Ads", href: "https://ads.google.com" }, { label: "Meta Ads Manager", href: "https://www.facebook.com/business/tools/ads-manager" }, { label: "LinkedIn Campaign Manager", href: "https://business.linkedin.com/marketing-solutions" }, { label: "Google Analytics", href: "https://analytics.google.com" }, ], offer: [ { highlight: "Campaign strategy", text: ": audiences, budgets and goals set from your margins, not guesswork." }, { highlight: "Ad creative", text: ": copy and visuals made for each channel and tested against each other." }, { highlight: "Weekly optimization", text: ": bids, keywords and placements tuned every week." }, { highlight: "Clear reporting", text: ": one dashboard and a monthly call in plain numbers." }, ], benefits: [ { highlight: "Traffic from day one", text: ": qualified visitors while search and social grow." }, { highlight: "Spend you can defend", text: ": every campaign tied to cost per lead and return." }, { highlight: "No lock-in", text: ": your accounts, your data, cancel any month." }, ], name: "Paid advertising package", plans: [ { id: "monthly", label: "Monthly", price: "$1,800", note: "per month, plus ad spend", included: ["Two channels", "Up to 12 ad variations", "Weekly optimization", "Monthly report and call"], }, { id: "quarterly", label: "Quarterly", price: "$4,900", note: "per quarter, plus ad spend — save 9%", included: ["Three channels", "Up to 30 ad variations", "Weekly optimization", "Quarterly strategy review"], }, ], action: { label: "Book an intro call", href: "/contact" }, }; export function ServicesPricing({ title = defaults.title, tools = defaults.tools, offer = defaults.offer, benefits = defaults.benefits, name = defaults.name, plans = defaults.plans, action = defaults.action, labels = defaults.labels, className, }: ServicesPricingProps) { const [planId, setPlanId] = React.useState(plans[0]?.id); const plan = plans.find((item) => item.id === planId) ?? plans[0]; return ( <section className={cn("py-(--section-y)", className)}> <h2 className="sr-only">{title}</h2> <div className="mx-auto flex max-w-(--container) flex-col gap-12 px-(--gutter) lg:flex-row lg:gap-24"> <div className="flex min-w-0 flex-col gap-12 lg:flex-1"> {tools.length > 0 && ( <div className="flex flex-col gap-2.5"> <h3 className="font-heading text-h3 font-semibold tracking-tight">{labels.tools}</h3> <hr className="border-border" /> <ul> {tools.map((tool) => ( <li key={tool.label}> {tool.href ? ( <a href={tool.href} target="_blank" rel="noopener noreferrer" className="flex items-center gap-5 border-b border-border py-2.5 transition-colors hover:text-foreground/80 hover:underline hover:underline-offset-4" > <span className="flex-1">{tool.label}</span> <span aria-hidden className="size-2 rounded-full bg-foreground" /> </a> ) : ( <span className="flex items-center gap-5 border-b border-border py-2.5"> <span className="flex-1">{tool.label}</span> <span aria-hidden className="size-2 rounded-full bg-foreground" /> </span> )} </li> ))} </ul> </div> )} {[ { heading: labels.offer, items: offer }, { heading: labels.benefits, items: benefits }, ].map( (group) => group.items.length > 0 && ( <div key={group.heading} className="flex flex-col gap-5"> <h3 className="font-heading text-h3 font-semibold tracking-tight">{group.heading}</h3> <ul className="list-disc space-y-2 pl-5 text-muted-foreground marker:text-foreground"> {group.items.map((item) => ( <li key={(item.highlight ?? "") + item.text}> {item.highlight && <strong className="font-semibold text-foreground">{item.highlight}</strong>} {item.text} </li> ))} </ul> </div> ), )} </div> {plan && ( <div className="flex w-full flex-col gap-8 rounded-xl border border-border bg-card p-6 text-card-foreground lg:w-100 lg:shrink-0"> <div className="flex flex-col items-start gap-5"> {plans.length > 1 && ( <SegmentedControl label={labels.plans} size="sm" value={plan.id} onValueChange={setPlanId} options={plans.map((item) => ({ value: item.id, label: item.label }))} /> )} <h3 className="font-heading text-h3 font-semibold tracking-tight">{name}</h3> </div> <div className="flex flex-col gap-4"> <p className="font-medium">{labels.included}</p> <ul className="flex flex-col gap-2.5" aria-live="polite"> {plan.included.map((item) => ( <li key={item} className="flex items-start gap-2.5 text-muted-foreground"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={1.75} strokeLinecap="round" strokeLinejoin="round" aria-hidden className="mt-1 size-4 shrink-0 text-foreground" > <path d="M3 8.5l3 3 7-7" /> </svg> <span className="min-w-0 flex-1">{item}</span> </li> ))} </ul> </div> <div className="flex flex-col items-start gap-5"> <div> <p className="font-heading text-h2 font-semibold tracking-tight">{plan.price}</p> <p className="text-sm text-muted-foreground">{plan.note}</p> </div> <SpringButton href={action.href}>{action.label}</SpringButton> </div> </div> )} </div> </section> ); }
Usage
import { ServicesPricing } from "@/components/services-pricing";
<ServicesPricing
name="Paid advertising package"
plans={[
{ id: "monthly", label: "Monthly", price: "$1,800", note: "per month", included: ["Two channels"] },
{ id: "quarterly", label: "Quarterly", price: "$4,900", note: "per quarter", included: ["Three channels"] },
]}
action={{ label: "Book an intro call", href: "/contact" }}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | — | Section heading for screen readers (visually hidden). |
| tools | { label, href? }[] | — | List with a dot per row; links open in a new tab. |
| offer / benefits | { highlight?, text }[] | — | Bulleted lists; highlight is bold and text follows it (start it with ": "). |
| name | string | — | Package name at the top of the card (h3). |
| plans | { id, label, price, note, included[] }[] | Monthly, Quarterly | The switch shows with more than one plan; each brings its own list, price and note. |
| action | { label, href } | — | Button at the bottom of the card. |
| labels | { tools, offer, benefits, included, plans } | — | Headings, and the switch's accessible name. |
| className | string | — | Classes for the <section>. |
- Below 1024px the card follows the text; from 1024px it is 400px wide beside it and as tall as it.
- The switch's fill slides to the chosen plan (segmented-control spring, bounce 0.15, 0.45s); arrow keys move between plans. The included list announces its change.
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS + Motion block called ServicesPricing (Next.js). - <section className="py-(--section-y)"> with a visually hidden h2; container mx-auto max-w-(--container) px-(--gutter); a column that becomes a row from 1024px (gap-24). - Left (flex-1, gap-12): "Tools" (h3 font-heading text-h3 semibold) over an <hr> and rows with border-b, label and an 8px dot; "What we offer" and "Key benefits" as h3 + a bulleted list (muted text, bold highlight in foreground). - Right: a card (rounded-xl border bg-card p-6, 400px wide from 1024px, stretched to the text's height, content at the top, gap-8): a segmented control of plans (radio group, the selected fill slides with a spring), the package name (h3), "What's included" with check icons, the price (font-heading text-h2) over a muted note, and a shadcn-style button with an arrow that turns 45° on hover. Switching plans swaps the list, price and note. - Only shadcn variables and block tokens.











