Pricing
Pricing は、料金プランをカードで並べて表示するサンプルです。
pricing/default is coming soon.
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import PricingCard from '$lib/components/ui/atoms/PricingCard.svelte';
import SegmentedControl, { type SegmentedControlItem } from '$lib/components/ui/atoms/SegmentedControl.svelte';
type BillingPeriod = 'monthly' | 'yearly';
let billingPeriod = $state<BillingPeriod>('monthly');
const billingSegments: SegmentedControlItem[] = [
{ id: 'monthly', label: '月払い' },
{ id: 'yearly', label: '年払い' },
];
const plansData = [
{
name: 'Free',
description: '最大5ユーザーが永遠に無料のプラン',
prices: { monthly: '¥0', yearly: '¥0' },
captions: { monthly: '1ユーザーあたり月額', yearly: '1ユーザーあたり年額' },
labels: [
{ label: 'ユーザー数', value: '5人' },
{ label: 'ストレージ', value: '5GB' },
],
features: [
{ label: '基本機能', enabled: true },
{ label: '管理者機能', enabled: false },
{ label: 'AI機能', enabled: false },
],
},
{
name: 'Standard',
description: 'ミニマムな管理機能のライトなプラン',
prices: { monthly: '¥980', yearly: '¥10,000' },
captions: { monthly: '1ユーザーあたり月額', yearly: '1ユーザーあたり年額' },
mostPopular: true,
labels: [
{ label: 'ユーザー数', value: '30人' },
{ label: 'ストレージ', value: '1TB' },
],
features: [
{ label: '基本機能', enabled: true },
{ label: '管理者機能', enabled: true },
{ label: 'AI機能', enabled: false },
],
},
{
name: 'Premium',
description: '管理機能とAI機能が充実したプラン',
prices: { monthly: '¥1,980', yearly: '¥20,000' },
captions: { monthly: '1ユーザーあたり月額', yearly: '1ユーザーあたり年額' },
labels: [
{ label: 'ユーザー数', value: '無制限' },
{ label: 'ストレージ', value: '100TB' },
],
features: [
{ label: '基本機能', enabled: true },
{ label: '管理者機能', enabled: true },
{ label: 'AI機能', enabled: true },
],
},
];
const plans = $derived(
plansData.map(({ prices, captions, ...plan }) => ({
...plan,
price: prices[billingPeriod],
caption: captions[billingPeriod],
})),
);
/**
* 月払い/年払いの切り替えが発生したとき。
* @param item 選択された SegmentedControl のセグメント項目
*/
function handleChangeBillingPeriod(item: SegmentedControlItem) {
if (item.id !== 'monthly' && item.id !== 'yearly') return;
billingPeriod = item.id;
}
</script>
<div class="flex flex-col items-center justify-center gap-8 size-full min-h-screen p-8">
<SegmentedControl segmentedControls={billingSegments} value={billingPeriod} onChange={handleChangeBillingPeriod} />
<div class="flex gap-3 max-md:flex-col w-full max-w-4xl">
{#each plans as plan}
<PricingCard class="flex-1 min-w-0" {plan}>
{#snippet button()}
<Button class="w-full" variant={plan.mostPopular ? 'primary' : 'secondary'} tone="solid" size="small">このプランではじめる</Button>
{/snippet}
</PricingCard>
{/each}
</div>
</div>
インストールの手順
以下のコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。
pricing/default is coming soon.
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import PricingCard from '$lib/components/ui/atoms/PricingCard.svelte';
import SegmentedControl, { type SegmentedControlItem } from '$lib/components/ui/atoms/SegmentedControl.svelte';
type BillingPeriod = 'monthly' | 'yearly';
let billingPeriod = $state<BillingPeriod>('monthly');
const billingSegments: SegmentedControlItem[] = [
{ id: 'monthly', label: '月払い' },
{ id: 'yearly', label: '年払い' },
];
const plansData = [
{
name: 'Free',
description: '最大5ユーザーが永遠に無料のプラン',
prices: { monthly: '¥0', yearly: '¥0' },
captions: { monthly: '1ユーザーあたり月額', yearly: '1ユーザーあたり年額' },
labels: [
{ label: 'ユーザー数', value: '5人' },
{ label: 'ストレージ', value: '5GB' },
],
features: [
{ label: '基本機能', enabled: true },
{ label: '管理者機能', enabled: false },
{ label: 'AI機能', enabled: false },
],
},
{
name: 'Standard',
description: 'ミニマムな管理機能のライトなプラン',
prices: { monthly: '¥980', yearly: '¥10,000' },
captions: { monthly: '1ユーザーあたり月額', yearly: '1ユーザーあたり年額' },
mostPopular: true,
labels: [
{ label: 'ユーザー数', value: '30人' },
{ label: 'ストレージ', value: '1TB' },
],
features: [
{ label: '基本機能', enabled: true },
{ label: '管理者機能', enabled: true },
{ label: 'AI機能', enabled: false },
],
},
{
name: 'Premium',
description: '管理機能とAI機能が充実したプラン',
prices: { monthly: '¥1,980', yearly: '¥20,000' },
captions: { monthly: '1ユーザーあたり月額', yearly: '1ユーザーあたり年額' },
labels: [
{ label: 'ユーザー数', value: '無制限' },
{ label: 'ストレージ', value: '100TB' },
],
features: [
{ label: '基本機能', enabled: true },
{ label: '管理者機能', enabled: true },
{ label: 'AI機能', enabled: true },
],
},
];
const plans = $derived(
plansData.map(({ prices, captions, ...plan }) => ({
...plan,
price: prices[billingPeriod],
caption: captions[billingPeriod],
})),
);
/**
* 月払い/年払いの切り替えが発生したとき。
* @param item 選択された SegmentedControl のセグメント項目
*/
function handleChangeBillingPeriod(item: SegmentedControlItem) {
if (item.id !== 'monthly' && item.id !== 'yearly') return;
billingPeriod = item.id;
}
</script>
<div class="flex flex-col items-center justify-center gap-8 size-full min-h-screen p-8">
<SegmentedControl segmentedControls={billingSegments} value={billingPeriod} onChange={handleChangeBillingPeriod} />
<div class="flex gap-3 max-md:flex-col w-full max-w-4xl">
{#each plans as plan}
<PricingCard class="flex-1 min-w-0" {plan}>
{#snippet button()}
<Button class="w-full" variant={plan.mostPopular ? 'primary' : 'secondary'} tone="solid" size="small">このプランではじめる</Button>
{/snippet}
</PricingCard>
{/each}
</div>
</div>
PricingCard
atoms/PricingCard.svelte
<!--
@component
## 概要
- 料金プランの情報をカード形式で表示するためのコンポーネントです
- mostPopular を true にすることで、強調スタイルで表示できます
## 機能
- プラン名・価格・機能リスト・ラベルリストなどをカードにまとめて表示します
- 見た目を変更するためのいくつかのスタイル用Propsが追加されています(詳細はPropsセクションを参照)
## Props
- plan: プランデータをオブジェクトとして指定します
- name: プラン名を指定します
- description: プランの説明文を指定します
- price: 価格を指定します(例: ¥980)
- caption: 価格の補足テキストを指定します(例: 1ユーザーあたり月額)
- mostPopular: true にするとプライマリカラーのボーダーとシャドウで強調スタイルになります
- features: 機能リストを指定します(enabled が false のものはグレーアウトされます)
- labels: ラベルリストを指定します
- button: CTAボタンを描画する snippet を指定します
## Usage
```svelte
<PricingCard {plan}>
{#snippet button()}
<Button class="w-full" variant="primary" tone="solid" size="small">このプランではじめる</Button>
{/snippet}
</PricingCard>
```
-->
<script module lang="ts">
import type { Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import { cva, type VariantProps } from 'class-variance-authority';
export const pricingCardVariants = cva('flex flex-col overflow-hidden rounded-lg border bg-surface', {
variants: {
/** 強調スタイルにするか */
mostPopular: {
true: 'border-primary shadow-md',
false: 'border-border',
},
},
defaultVariants: {
mostPopular: false,
},
});
export type PricingCardVariants = VariantProps<typeof pricingCardVariants>;
export interface PricingCardFeature {
/** 機能名 */
label: string;
/** このプランに含まれるか(false の場合グレーアウト) */
enabled?: boolean;
}
export interface PricingCardLabel {
/** 項目名 */
label: string;
/** 値 */
value: string;
}
export interface PricingCardPlan {
/** プラン名 */
name: string;
/** プランの説明文 */
description?: string;
/** 価格表示(例: ¥980) */
price: string;
/** 価格の補足テキスト(例: 1ユーザーあたり月額) */
caption?: string;
/** 強調スタイルにするか */
mostPopular?: boolean;
/** 機能リスト */
features?: PricingCardFeature[];
/** ラベルリスト */
labels?: PricingCardLabel[];
}
export interface PricingCardProps {
/** クラス */
class?: ClassValue;
/** プランデータ */
plan: PricingCardPlan;
/** CTAボタン */
button?: Snippet<[]>;
}
</script>
<script lang="ts">
import Separator from '$lib/components/ui/atoms/Separator.svelte';
import Check from '@lucide/svelte/icons/check';
let { class: className, plan, button }: PricingCardProps = $props();
let cardClass = $derived(pricingCardVariants({ mostPopular: plan.mostPopular, class: className }));
</script>
<div class={cardClass} data-rabee-ui="pricing-card">
<div class="flex flex-col items-center gap-3 px-6 pb-3 pt-5 text-center">
<div class="flex flex-col w-full">
<p class="text-lg/tight font-semibold text-foreground">{plan.name}</p>
{#if plan.description}
<p class="text-sm text-muted-foreground">{plan.description}</p>
{/if}
</div>
<div class="flex flex-col w-full">
<p class="text-3xl/tight font-semibold text-foreground">{plan.price}</p>
{#if plan.caption}
<p class="text-sm text-muted-foreground">{plan.caption}</p>
{/if}
</div>
{#if button}
{@render button()}
{/if}
</div>
{#if plan.labels?.length || plan.features?.length}
<Separator />
<div class="flex flex-1 flex-col gap-4 p-6">
{#if plan.labels?.length}
<div class="flex flex-col gap-1">
{#each plan.labels as { label, value }}
<div class="flex items-center">
<span class="w-1/2 text-sm text-muted-foreground">{label}</span>
<span class="w-1/2 text-sm text-foreground">{value}</span>
</div>
{/each}
</div>
{/if}
{#if plan.features?.length}
<div class="flex flex-col gap-1">
{#each plan.features as feature}
<div class={['flex items-start gap-1 py-1', feature.enabled === false && 'opacity-50']}>
<Check class={['mt-0.5 shrink-0', feature.enabled !== false ? 'text-primary' : 'text-foreground']} size="1rem" />
<span class={['text-sm', feature.enabled !== false ? 'text-primary' : 'text-foreground']}>{feature.label}</span>
</div>
{/each}
</div>
{/if}
</div>
{/if}
</div>
依存コンポーネント
Pricing を使うときは、以下のコンポーネントもダウンロードが必要です。