Patterns · Surfaces
Paywall
Wrap a pro-gated affordance; the shared upsell renders itself — the crown upgrade mark, title, one-sentence body, the fixed unlock line, and Upgrade + See all plans CTAs are baked in.
Examples
Plan
Feature
Placement
<!-- Paste into an .html file and open — no build step. Wrap a gated affordance; the upsell renders itself. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ahaslides-product/design/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/npm/@ahaslides-product/design/lib/aha-paywall.js'; // registers <aha-paywall>
</script>
<!-- Pro gate — the affordance is the anchor. The popover ships the crown mark, title, description,
the fixed unlock line, and BOTH CTAs (Upgrade positive + See all plans) — all baked in. -->
<aha-paywall
feature-key="streak_bonus"
feature-label="Streak answer bonus point"
body="Let the participants play quiz and be ranked as teams instead of being ranked individually."
required-plan="pro">
<button>Streak bonus</button> <!-- the gated affordance (omit for a crown upgrade-mark anchor) -->
</aha-paywall>
<!-- No natural anchor → the crown upgrade mark IS the anchor (a focusable control); hover-triggered -->
<aha-paywall
feature-key="export_results"
feature-label="Export results"
body="Download every response as a CSV or PDF."
required-plan="essential"
trigger="hover"></aha-paywall>
<script>
// analytics contract — the app wires these to its tracker (fires for every paywall)
document.querySelectorAll('aha-paywall').forEach((el) => {
el.addEventListener('paywall-shown', (e) => console.log('PAYWALL_SHOWN', e.detail));
el.addEventListener('paywall-upgrade', (e) => console.log('PAYWALL_UPGRADE_CLICKED', e.detail));
el.addEventListener('paywall-plans', (e) => console.log('PAYWALL_PLANS_CLICKED', e.detail)); // See all plans
});
</script>
import '@ahaslides-product/design/aha-paywall'; // registers <aha-paywall>
import '@ahaslides-product/design/tokens.css';
import { useRef, useEffect } from 'react';
// Wrap the gated affordance; the element renders the compliant upsell (crown badge, one-sentence
// body, fixed unlock line, single Upgrade CTA are baked in). analytics comes back as events.
export function CustomUrlSetting() {
const ref = useRef(null);
useEffect(() => {
const el = ref.current, track = (name) => (e) => window.analytics?.track(name, e.detail);
el.addEventListener('paywall-shown', track('PAYWALL_SHOWN'));
el.addEventListener('paywall-upgrade', track('PAYWALL_UPGRADE_CLICKED'));
el.addEventListener('paywall-plans', track('PAYWALL_PLANS_CLICKED')); // See all plans
}, []);
return (
<>
{/* Pro gate — the affordance is the anchor. Popover ships crown mark, title, description,
the fixed unlock line, and BOTH CTAs (Upgrade positive + See all plans). */}
<aha-paywall
ref={ref}
feature-key="streak_bonus"
feature-label="Streak answer bonus point"
body="Let the participants play quiz and be ranked as teams instead of being ranked individually."
required-plan="pro">
<button>Streak bonus</button>
</aha-paywall>
{/* Essential gate, no natural anchor → the crown badge is the anchor */}
<aha-paywall
feature-key="export_results"
feature-label="Export results"
body="Download every response as a CSV or PDF."
required-plan="essential" />
</>
);
}
<script setup>
import '@ahaslides-product/design/aha-paywall'; // registers <aha-paywall>
import '@ahaslides-product/design/tokens.css';
// The element renders the compliant upsell; analytics comes back as events.
const onShown = (e) => window.analytics?.track('PAYWALL_SHOWN', e.detail);
const onUpgrade = (e) => window.analytics?.track('PAYWALL_UPGRADE_CLICKED', e.detail);
const onPlans = (e) => window.analytics?.track('PAYWALL_PLANS_CLICKED', e.detail); // See all plans
</script>
<template>
<!-- Pro gate — the affordance is the anchor. Popover ships crown mark, title, description,
the fixed unlock line, and BOTH CTAs (Upgrade positive + See all plans). -->
<aha-paywall
feature-key="streak_bonus"
feature-label="Streak answer bonus point"
body="Let the participants play quiz and be ranked as teams instead of being ranked individually."
required-plan="pro"
@paywall-shown="onShown"
@paywall-upgrade="onUpgrade"
@paywall-plans="onPlans">
<button>Streak bonus</button>
</aha-paywall>
<!-- No natural anchor → the crown upgrade mark is the anchor (a focusable control) -->
<aha-paywall
feature-key="export_results"
feature-label="Export results"
body="Download every response as a CSV or PDF."
required-plan="essential"
@paywall-shown="onShown"
@paywall-upgrade="onUpgrade"
@paywall-plans="onPlans" />
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
feature-key | string (snake_case) | — | Stable token (e.g. custom_survey_url) — drives analytics + DOM hooks. Required. |
feature-label | string | Pro feature | Short noun-phrase title next to the crown (used as the popover title unless title is set) |
title | string | — | Popover title override; falls back to feature-label. May wrap to two lines |
body | string | — | Exactly one sentence describing the benefit of unlocking; ends with a period |
description | string | — | Popover description override; falls back to body |
required-plan | essential | pro | pro | Drives the unlock line and the analytics plan prop |
placement | top | bottom | top | Popover side relative to the anchor |
trigger | click | hover | click | How the popover opens; click-away + Escape close a click popover |
cta-label | string | Upgrade | The positive CTA verb; override only with a clear reason |
plans-label | string | See all plans | The secondary CTA label — routes to the full pricing page |
upgrade-url | string (URL) | ahaslides.com/pricing | Where Upgrade navigates (new tab, noopener) |
plans-url | string (URL) | ahaslides.com/pricing | Where See all plans navigates (new tab, noopener) |
slot (default) | element | upgrade mark | The gated affordance = the anchor; omit it to use the crown upgrade mark as the anchor |
paywall-shown / paywall-upgrade / paywall-plans | CustomEvent<{feature,plan}> | — | Analytics contract; paywall-upgrade + paywall-plans are cancelable (preventDefault to run your own flow) |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-paywall'; // registers <aha-paywall>
Agent feed for this component (absolute, fetchable anywhere): paywall.agent.json · paywall.md · paywall.llms.txt
When to use
When to use
- aha-paywall — any feature locked behind a paid plan — editor affordance, share option, results feature, dashboard action
- crown-badge anchor — the feature has no natural clickable anchor (e.g. a field label) — omit the slot and the crown is the anchor
- required-plan pro — the default gate; use essential only for essential-tier features
- never a bespoke upsell — a hand-rolled upgrade modal/tooltip drifts from the presenter app — always this element
Feature label is a short noun phrase; body is exactly one sentence about the benefit (not "This is a Pro feature"); the unlock line is fixed — do not reword. Popover/placement mechanics defer to aha-design-overlays; a single plan-gated setting row lives in the Settings surface.
Surfaces
editor dashboard settings audience
Spec
Upgrade mark 16×16 purple circle (--aha-color-primary) + white crown — a focusable control with Default/Hover/Active states; the custom mark, not a Phosphor crown · Popover dark indigo surface #242442 (--aha-bg-dark-raised), 300px wide, 16px padding, radius 12; animates opacity + lift on open via the motion tokens · Anatomy header (crown + title) · body (one sentence + "Unlock with the <Plan> plan.") · footer (Upgrade + See all plans) · Buttons TWO shared <aha-button>s — Upgrade (variant=positive, green --aha-button-positive-bg) + See all plans (variant=secondary, re-themed for the dark surface); never hand-rolled · Analytics paywall-shown on open · paywall-upgrade before Upgrade nav · paywall-plans before See-all-plans nav — all { feature, plan } · Behaviour Upgrade opens ahaslides.com/pricing (new tab, noopener); See all plans opens plans-url; preventDefault either event to run your own flow. Esc / click-away close