PricingCard
One plan: name, price and period, what it includes, and a button; highlighted with a badge when it is the one to pick.
Demo
Hobby
For trying things out.
- 3 projects
- 1 GB storage
- Custom domain (not included)
- Priority support (not included)
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.qwemain.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>}Related
PriceTagA bold price with its period (/month) and a note under it, for plans, rooms and seats.6 views · 4 downloadsPricingTablePlans across and features down, with ticks, dashes and values in the cells and the recommended plan marked.7 views · 6 downloadsProductCardA product in a shop's grid: picture, name, price with the discount, stars, a badge and Add to cart, as a plain form or a callback.6 views · 6 downloadsProductGridProducts as ProductCards in a grid that fits as many across as there is room for, from a list of records.6 views · 5 downloadsSubscriptionCardThe plan someone is on: price, status, renewal date, usage bars and the links to manage, change or cancel it.6 views · 6 downloadsAddressCardA saved address with a Default badge and Edit and Remove, as submit buttons in a plain form or callbacks.6 views · 7 downloads
Thread
Thread 0 posts
On PricingCard: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.