form agree-form
Agree Form — 利用規約・PP・特商法 同意フォーム
利用規約 + プライバシーポリシー + 特定商取引法に基づく表記の 3 点セット同意 + 動詞 CTA。disabled 禁止のため未同意送信は inline error で誘導。
日本特化無料MITv1.1.0
コード · components/blocks/agree-form.tsx
"use client";
import { useState, type FormEvent } from "react";
import Link from "next/link";
import { AlertCircle, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
/**
* Shoji agree-form — Japanese consent / 特商法 acknowledgement block.
*
* Surface B (Blocks) contract — design-spec §4.5:
* - shadcn primitives only (Button, Checkbox, Label)
* - shadcn semantic tokens only (bg-background, text-foreground, border, text-destructive)
* - Tailwind default scale + default radius — no Shoji-specific overrides
*
* What this block solves (ja_specific = true):
* 1. 利用規約 / プライバシーポリシー / 特定商取引法に基づく表記 を 1 ブロックで提示
* (消費者契約法・電子契約法・特商法の3点セットを Form 単位で満たす雛形)
* 2. `disabled` でボタンを殺さない — 未同意のまま送信したら inline-error で誘導
* (prohibited.md: disabled 禁止 / エラーは 色 + アイコン + テキストの 3 点セット)
* 3. 同意は単一 checkbox(3つ全部に同意)で十分。粒度を分けたい場合は派生ブロックへ
* 4. CTA は「動詞 + 目的語」の "申し込みを確定する" を採用(「OK」「同意」NG)
* 5. リンクはすべて external 想定 — `target="_blank"` + `rel="noreferrer"` で別タブ
*/
export function AgreeForm() {
const [agreed, setAgreed] = useState(false);
const [showError, setShowError] = useState(false);
const [submitted, setSubmitted] = useState(false);
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!agreed) {
setShowError(true);
return;
}
setShowError(false);
setSubmitted(true);
};
return (
<section className="bg-background text-foreground">
<div className="mx-auto w-full max-w-xl px-6 py-16 lg:py-24">
<header className="mb-10 flex flex-col gap-2">
<p className="font-mono text-xs uppercase tracking-[0.18em] text-muted-foreground">
Form · 日本特化
</p>
<h2 className="text-2xl font-semibold leading-[1.3] tracking-tight lg:text-3xl">
お申し込み内容の確認
</h2>
<p className="text-sm leading-[1.75] text-muted-foreground">
以下の規約・表記をご確認のうえ、同意して申し込みを確定してください。
</p>
</header>
<form
onSubmit={onSubmit}
noValidate
className="flex flex-col gap-8 rounded-lg border border-border bg-card p-6 sm:p-8"
>
<ul className="flex flex-col gap-2 text-sm leading-[1.75]">
<li className="flex items-baseline justify-between gap-4 border-b border-border pb-2">
<span className="text-foreground">利用規約</span>
<Link
href="/terms"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 font-medium text-foreground underline-offset-4 hover:underline"
>
内容を見る
<ArrowRight aria-hidden className="h-3.5 w-3.5" />
</Link>
</li>
<li className="flex items-baseline justify-between gap-4 border-b border-border pb-2">
<span className="text-foreground">プライバシーポリシー</span>
<Link
href="/privacy"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 font-medium text-foreground underline-offset-4 hover:underline"
>
内容を見る
<ArrowRight aria-hidden className="h-3.5 w-3.5" />
</Link>
</li>
<li className="flex items-baseline justify-between gap-4 pb-1">
<span className="text-foreground">
特定商取引法に基づく表記
</span>
<Link
href="/legal/tokushoho"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 font-medium text-foreground underline-offset-4 hover:underline"
>
内容を見る
<ArrowRight aria-hidden className="h-3.5 w-3.5" />
</Link>
</li>
</ul>
<div className="flex flex-col gap-2">
<div className="flex items-start gap-3">
<Checkbox
id="agree-form-consent"
checked={agreed}
onCheckedChange={(state) => {
const next = state === true;
setAgreed(next);
if (next) setShowError(false);
}}
aria-describedby={
showError ? "agree-form-consent-error" : undefined
}
aria-invalid={showError ? true : undefined}
className={
showError
? "border-destructive data-[state=unchecked]:border-destructive"
: undefined
}
/>
<Label
htmlFor="agree-form-consent"
className="text-sm leading-[1.75] text-foreground"
>
上記の利用規約・プライバシーポリシー・特定商取引法に基づく表記に同意します。
</Label>
</div>
{showError ? (
<p
id="agree-form-consent-error"
className="ml-7 flex items-start gap-1.5 text-sm text-destructive"
>
<AlertCircle
aria-hidden
className="mt-0.5 h-3.5 w-3.5 shrink-0"
/>
<span>同意いただかないと申し込みを確定できません。</span>
</p>
) : null}
</div>
<div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-end">
<Button type="button" variant="ghost">
戻って内容を見直す
</Button>
<Button type="submit" size="lg">
申し込みを確定する
<ArrowRight aria-hidden className="h-4 w-4" />
</Button>
</div>
{submitted ? (
<p
role="status"
className="rounded-md border border-border bg-muted/40 px-4 py-3 text-sm leading-[1.75] text-foreground"
>
ご同意ありがとうございます。続けて確認画面に進みます。
</p>
) : null}
</form>
</div>
</section>
);
}
AI prompt · MDX
shoji_block: agree-form---
shoji_block: agree-form
shoji_version: 1.1.0
category: form
license: MIT
dependencies: ["lucide-react", "next"]
shadcn_primitives: ["button", "checkbox", "label"]
ja_specific: true
---
# agree-form
> Shoji block for AI agents. Paste this MDX to your AI; it scaffolds a Japanese
> consent / 特商法 acknowledgement form covering 利用規約 + プライバシーポリシー +
> 特定商取引法に基づく表記.
## Component
```tsx
"use client";
import { useState, type FormEvent } from "react";
import Link from "next/link";
import { AlertCircle, ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
export function AgreeForm() {
const [agreed, setAgreed] = useState(false);
const [showError, setShowError] = useState(false);
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!agreed) {
setShowError(true);
return;
}
// proceed
};
// 利用規約 / プライバシーポリシー / 特商法 へのリンク列 + 同意 checkbox + 動詞 CTA
}
```
## Usage
```tsx
import { AgreeForm } from "@/components/blocks/agree-form";
export default function Page() {
return <AgreeForm />;
}
```
## Block contract (Two-surfaces — design-spec §4.5)
This is a **Surface B block** — it inherits your shadcn theme.
- shadcn primitives only (`Button`, `Checkbox`, `Label`)
- shadcn semantic tokens only (`bg-background`, `text-foreground`, `border`, `text-destructive`)
- Tailwind default scale + your shadcn radius
## What it solves (ja_specific = true)
1. **3-link consent block**: 利用規約 + プライバシーポリシー + 特定商取引法に基づく表記 are all required for B2C JP services. Most templates only show 2.
2. **No `disabled` button**: prohibited.md bans `disabled` for "submit blocked by missing consent". Instead, the CTA stays active and surfaces an inline error explaining what to fix.
3. **3-point error pattern**: error uses color (`text-destructive`) + icon (`AlertCircle`) + descriptive text. Never color alone.
4. **Verb + object CTA**: "申し込みを確定する" instead of "OK" / "同意" / "送信" — per UX guidelines.
5. **External-link defaults**: each policy link opens in a new tab with `rel="noreferrer"` so users keep their form state.
## Customization hints
- **Per-policy granularity**: split the single checkbox into 3 separate ones if your service requires per-document consent (some 特定商取引法 readings demand it for high-value contracts).
- **Link destinations**: replace `/terms` / `/privacy` / `/legal/tokushoho` with your actual routes, or wire to a CMS-driven policy page.
- **Reset error on type**: the block clears `showError` as soon as the checkbox flips to checked — extend this if your form gains other fields with their own validation.
- **Confirmation modal**: the `submitted` `<p role="status">` is a placeholder. Wire it into your own confirm-step modal or routed `/confirm` page.
## A11y
- Checkbox uses an explicit `<Label htmlFor>` with full sentence, not a placeholder-as-label.
- `aria-describedby` + `aria-invalid` connect the error message to the checkbox for screen readers.
- The success `<p role="status">` is announced as a live region without stealing focus.
- Two CTAs (Ghost + Primary) form a hierarchy — never two Primary side-by-side per UX guidelines.
- Each policy link includes inline icon + text — color is never the only affordance.
## Why this is a Shoji block
- **JP-tuned by default.** The 3-document set (規約 + PP + 特商法) is the floor for any Japanese paid service. Shoji bakes it in so projects can't ship without it.
- **AI agent ready.** This MDX is paste-ready; the prohibited.md rationale is encoded in the JSX so generated code inherits the right defaults.
- **No theming opinions.** Pure shadcn primitives + semantic tokens — drop into any shadcn project.