Commerce

PricingCard

One plan: name, price and period, what it includes, and a button; highlighted with a badge when it is the one to pick.

#commerce#price#card#marketing#no-js

Demo

Preview

Hobby

For trying things out.

  • 3 projects
  • 1 GB storage
  • Custom domain (not included)
  • Priority support (not included)
Most popular

Pro

For people who ship.

billed yearly
  • Unlimited projects
  • 50 GB storage
  • Custom domain
  • Dedicated manager (not included)

Team

For up to 5 people.

billed monthly
  • Everything in Pro
  • 5 seats
  • Shared workspaces
  • SSOSAML and OIDC

Cancel any time.

Not chosen (on_select).

Get it

It needs theme.qwe, as every component does, and price.qwe and price_tag.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/price.qwe$ curl -O https://quillolang.org/components/ui/price_tag.qwe$ curl -O https://quillolang.org/components/ui/pricing_card.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/pricing_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/pricing_card.qwe
component PricingCardDemoLive() {    state chosen = null    <div.demo-stack style="width: 260px">        <PricingCard name="Team" price={49} period="month" description="For up to 5 people." note="billed monthly"                     features={["Everything in Pro", "5 seats", "Shared workspaces", { label: "SSO", hint: "SAML and OIDC" }]}                     footnote="Cancel any time." on_select={() => { chosen = "Team" }}/>        <p.demo-note>{chosen == null ? "Not chosen (on_select)." : `Chose ${chosen}.`}</p>    </div>}view PricingCardDemo() =    <div.demo-row style="align-items: stretch; gap: 20px; padding-top: 12px">        <div style="width: 260px">            <PricingCard name="Hobby" price={0} period="month" description="For trying things out." button="Start free" href="#pricing_card" current={true}                         features={["3 projects", "1 GB storage", { label: "Custom domain", included: false }, { label: "Priority support", included: false }]}/>        </div>        <div style="width: 260px">            <PricingCard name="Pro" price={12} old={15} period="month" description="For people who ship." note="billed yearly"                         highlighted={true} badge="Most popular" action="/signup"                         features={["Unlimited projects", "50 GB storage", "Custom domain", { label: "Dedicated manager", included: false }]}/>        </div>        <PricingCardDemoLive/>    </div>

Source

ui/pricing_card.qwe
// PricingCard: one plan: its name, price and period, what it includes, and a// button; the one to pick is highlighted. The price is a PriceTag (this file// needs price_tag.qwe and price.qwe).////   <PricingCard name="Pro" price={12} period="month" note="billed yearly" href="/signup?plan=pro"//                highlighted={true} badge="Most popular"//                features={["Unlimited projects", "50 GB storage", { label: "Dedicated manager", included: false }]}/>//// name: the plan's name. description: a line under it. price, old, period,// prefix, note, currency, locale, cents (false): as for PriceTag. features:// strings, or { label, included?, hint? } (included: false shows it greyed// with a dash). href makes the button a link; action a form that posts the// plan's `name` as `plan`; otherwise, inside one of your components, the// button calls on_select(). button: its text ("Choose Pro"). highlighted: the// accent border and a solid button. badge: a label on the top edge ("Most// popular"). current: says "Current plan" and disables the button. footnote: a// quieter line at the bottom.import ui/price_tag.qwe use PriceTagimport ui/theme.qwe use ui_clslet pricing_card_css = css`.ui-plan { position: relative; display: flex; flex-direction: column; gap: 14px; min-width: 0; padding: 22px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.5 var(--ui-font); }.ui-plan-hi { border-color: var(--ui-accent); box-shadow: 0 0 0 1px var(--ui-accent), var(--ui-shadow); }.ui-plan-badge { position: absolute; top: -11px; left: 50%; transform: translateX(-50%); padding: 2px 10px; border-radius: 999px; background: var(--ui-accent); color: var(--ui-accent-fg);  font: 600 11px/1.6 var(--ui-font); text-transform: uppercase; letter-spacing: .04em; white-space: nowrap; }.ui-plan-head { display: grid; gap: 2px; }.ui-plan-name { margin: 0; font-size: 18px; font-weight: 600; }.ui-plan-desc { margin: 0; color: var(--ui-muted); }.ui-plan-features { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }.ui-plan-feature { display: flex; align-items: flex-start; gap: 8px; }.ui-plan-feature svg { flex: none; width: 18px; height: 18px; margin-top: 2px; color: var(--ui-success); }.ui-plan-feature-no { color: var(--ui-muted); }.ui-plan-feature-no svg { color: var(--ui-strong); }.ui-plan-hint { display: block; font-size: 12px; color: var(--ui-muted); }.ui-plan-form { display: contents; }.ui-plan-btn { display: flex; align-items: center; justify-content: center; width: 100%; height: 40px; margin-top: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm);  background: var(--ui-surface); color: var(--ui-fg); font: 600 14px/1 var(--ui-font); text-decoration: none; cursor: pointer; transition: background .15s, border-color .15s, box-shadow .15s; }.ui-plan-btn:hover { border-color: var(--ui-strong); background: var(--ui-subtle); }.ui-plan-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-plan-hi .ui-plan-btn { background: var(--ui-accent); color: var(--ui-accent-fg); border-color: transparent; }.ui-plan-hi .ui-plan-btn:hover { background: color-mix(in srgb, var(--ui-accent) 88%, var(--ui-fg)); }.ui-plan-btn[disabled], .ui-plan-btn[aria-disabled="true"] { opacity: .6; cursor: default; pointer-events: none; }.ui-plan-foot { margin: 0; font-size: 12px; color: var(--ui-muted); text-align: center; }@media (prefers-reduced-motion: reduce) { .ui-plan-btn { transition: none; } }`view PricingCard(name: str, price, old = null, period: str? = null, prefix: str? = null, note: str? = null, description: str? = null,                 features: list = [], href: str? = null, action: str? = null, button: str? = null, highlighted: bool = false,                 badge: str? = null, current: bool = false, footnote: str? = null, currency: str = "USD", locale: str? = null,                 cents: bool = false, on_select = null, class: str? = null) {    style global pricing_card_css    let text = current ? "Current plan" : (button ?? `Choose ${name}`)    let btn = href != null && !current        ? <a.ui-plan-btn href={href}>{text}</a>        : <button.ui-plan-btn type={action != null ? "submit" : "button"} disabled={current} aria-disabled={current ? "true" : null}                  @click={() => { if action == null && on_select != null { on_select() } }}>{text}</button>    <section class={ui_cls(["ui-plan", highlighted && "ui-plan-hi", class])} aria-label={`${name} plan`}>        {badge != null && <span.ui-plan-badge>{badge}</span>}        <div.ui-plan-head>            <h3.ui-plan-name>{name}</h3>            {description != null && <p.ui-plan-desc>{description}</p>}        </div>        <PriceTag amount={price} old={old} period={period} prefix={prefix} note={note} currency={currency} locale={locale} cents={cents}/>        {len(features) > 0 && <ul.ui-plan-features>            {for f in features => {                let it = type_of(f) == "map" ? f : { label: str(f) }                let no = get(it, "included") == false                <li key={it.label} class={ui_cls(["ui-plan-feature", no && "ui-plan-feature-no"])}>                    {no                        ? <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 12h12"/></svg>                        : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12.5 4.5 4.5L19 7.5"/></svg>}                    <span>{it.label}{no && <span.ui-sr> (not included)</span>}{get(it, "hint") != null && <span.ui-plan-hint>{it.hint}</span>}</span>                </li>            }}        </ul>}        {action != null ? <form.ui-plan-form method="post" action={action}><input type="hidden" name="plan" value={name}/>{btn}</form> : btn}        {footnote != null && <p.ui-plan-foot>{footnote}</p>}    </section>}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On PricingCard: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.