Billing period switch, such as monthly and yearly, built on the stock Tabs list. A controlled client component that takes a typed value and a change handler.
"use client"
import type { ComponentProps } from "react"
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
/**
* The switch between billing periods, such as monthly and annual, above a
* set of pricing plans. Put a `PricingToggleItem` inside for each period. It
* is a controlled tab list: keep the chosen period in state, pass it as
* `value` and update it in `onValueChange`.
*
* `className`, `aria-label` and other props land on the tab list. The toggle
* centers itself (`self-center`) in a flex or grid parent.
*/
function PricingToggle<Value extends string>({
value,
onValueChange,
...props
}: Omit<ComponentProps<typeof TabsList>, "variant"> & {
/**
* The stock `TabsList` variant: `default` (the default) sets the periods
* on a muted pill track, `line` drops the track and underlines the chosen
* period.
*/
variant?: ComponentProps<typeof TabsList>["variant"]
/** The selected period. It matches one `PricingToggleItem`'s `value`. */
value: Value
/** Called with the `value` of the period the visitor picks. */
onValueChange: (value: Value) => void
}) {
return (
<Tabs
value={value}
onValueChange={(next) => onValueChange(next as Value)}
className="self-center"
>
<TabsList data-slot="pricing-toggle" {...props} />
</Tabs>
)
}
/**
* One billing period inside `PricingToggle`, such as "Monthly". Give it a
* `value`; `PricingToggle` reports that value when it is picked.
*/
function PricingToggleItem(props: ComponentProps<typeof TabsTrigger>) {
return <TabsTrigger data-slot="pricing-toggle-item" {...props} />
}
export { PricingToggle, PricingToggleItem }
pnpm dlx shadcn add @01orbital/pricing-togglenpx shadcn add @01orbital/pricing-toggleyarn shadcn add @01orbital/pricing-togglebunx --bun shadcn add @01orbital/pricing-toggleFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/editorial/pricing-toggle.tsx, as source you own and edit.
"use client"
import { useState } from "react"
import {
PricingToggle,
PricingToggleItem,
} from "@/components/editorial/pricing-toggle"
const COPY = {
label: "Billing period",
monthly: "Monthly",
annual: "Yearly",
} as const
export default function PricingToggleDemo() {
const [period, setPeriod] = useState<"monthly" | "annual">("monthly")
return (
<PricingToggle
aria-label={COPY.label}
value={period}
onValueChange={setPeriod}
>
<PricingToggleItem value="monthly">{COPY.monthly}</PricingToggleItem>
<PricingToggleItem value="annual">{COPY.annual}</PricingToggleItem>
</PricingToggle>
)
}Pricing Toggle is built from 2 parts. In the example they nest like this:
PricingToggle
└─ PricingToggleItemPricingToggle: The switch between billing periods, such as monthly and annual, above a set of pricing plans. Put a PricingToggleItem inside for each period. It is a controlled tab list: keep the chosen period in state, pass it as value and update it in onValueChange.PricingToggleItem: One billing period inside PricingToggle, such as "Monthly". Give it a value; PricingToggle reports that value when it is picked.The switch between billing periods, such as monthly and annual, above a set of pricing plans. Put a PricingToggleItem inside for each period. It is a controlled tab list: keep the chosen period in state, pass it as value and update it in onValueChange.
className, aria-label and other props land on the tab list. The toggle centers itself (self-center) in a flex or grid parent.
Prop
Type
It also accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.
One billing period inside PricingToggle, such as "Monthly". Give it a value; PricingToggle reports that value when it is picked.
It accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.
Registry pieces it installs with it:
tabsPricing
Pricing card grid on the stock Card, using subgrid rows so titles, prices, descriptions and features align across plans. Gapped cards or a cells plate with hairline gaps; featured is a styling hook.
Quote
Testimonial parts: a grid, divided or masonry group whose one to four columns step on a parent Section; figures in default, centred or muted tile style; quotes in four sizes with a byline.