フォーム · お問い合わせフォーム
未保存の変更を見失わせないSaaS設定画面
左に節の目次、各節は左に説明・右に入力の2列で組むダークテーマのSaaS設定画面。変更した項目の数を数える未保存バー、役割を選べるメンバー一覧、名前を打ち込むまで押せない削除ボタンまでを備え、設定を触る人の不安を順に潰していく。
このブラウザー内に保存します。別の端末との同期はなく、サイトデータを消すと保存も消えます。
印刷時は見本を静止画にし、プロンプトと解説を出力します。PDF保存はブラウザーの印刷画面から選べます。
Vibehaus · デザイン資料
https://vibe.itlibra.com/design/saas-settings-form/
完全無料・著作権フリー(CC0)· 料金¥0 · 商用利用・改変自由
プレビュー
指定幅で実際に組み替えて表示します。表示領域より広い場合は、この枠の中で横へスクロールできます。
AIへの指示文
B2B SaaSのワークスペース設定画面を作ってください。ダークテーマです。 要件: - 配色: 地 #0b0c0f、入力欄 #121318、浮いた面 #181a20、罫線 #24262d、文字 #ececf1、補助 #a0a3ad、アクセント #7c8cff、危険 #f0525a。 - 左に幅220pxの目次(一般/メンバー/通知/危険な操作)。スクロールに合わせて現在の節を aria-current で示す。危険な操作だけ赤い文字。 - 各節は2列。左に節の見出しと「この設定を変えると何が起きるか」の説明、右に入力欄。幅860px未満では1列にする。 - 一般: ワークスペース名、URL(「app.example.jp/」の固定部分を入力欄の左に付けた形)、タイムゾーンの選択。入力欄の下に形式の説明を小さく。 - メンバー: アイコン・名前・メールアドレス・役割の選択・「外す」ボタンの一覧。自分の行は外すボタンの代わりに「あなた」。役割の選択には、見えないラベルで「〇〇 の役割」を付ける。 - 通知: チェックボックスに、太字の見出しと補足の2段のラベル。 - 危険な操作: 薄い赤の面と赤い枠。消えるデータの件数を具体的に書き、ワークスペース名を正確に打ち込むまで削除ボタンを押せなくする。 - 未保存の変更バー: 画面下中央に浮かせ、変更した項目の数を出す。保存した値といまの値を比べて数えるので、元に戻したら件数が減り、0になったらバーが消える。「取り消す」と「変更を保存」を置く。 - 未保存のまま離れようとしたら beforeunload で止める。保存したら role="status" で「保存しました」と知らせる。 - フォント: Inter と日本語の system フォント。
バリエーション
ライト版
地を #ffffff、入力欄を #fafafa、罫線を #e4e4e7、文字を #18181b に。 危険な操作の面は #fef2f2、枠は #fecaca にする。
節ごとに保存する版
未保存の変更バーをやめ、各節の右下に「保存」ボタンを置く。 その節の値が変わったときだけ押せるようにし、保存すると節の見出しの横に「保存しました」を2秒出す。
このプレビューのコード
HTML・CSS・JavaScriptの1ファイルです。外部依存・広告・解析コードはありません。変更数・取り消し・ページ内の仮保存を試せます。再読み込みで初期値へ戻り、認証・永続保存・招待・削除は行いません。
コードを表示
<!doctype html>
<!-- Vibehaus / CC0。HTML・CSS・JSの独立したデモ。保存はページ内だけ、外部通信なし。 -->
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex" />
<title>未保存の変更を見失わせないSaaS設定画面 — Vibehaus プレビュー</title>
<style>
:root {
--bg: #0b0c0f;
--panel: #121318;
--raised: #181a20;
--line: #24262d;
--line-2: #30333b;
--ink: #ececf1;
--muted: #a0a3ad;
--quiet: #8a8e99;
--accent: #7c8cff;
--accent-ink: #0b0c0f;
--danger: #f0525a;
--danger-soft: rgba(240, 82, 90, 0.08);
--font: "Inter", system-ui, -apple-system, "Hiragino Sans", "Hiragino Kaku Gothic ProN",
"Yu Gothic Medium", "Yu Gothic", "Meiryo", sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: var(--font);
font-size: 14px;
line-height: 1.75;
letter-spacing: 0;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
}
button, input, select { font: inherit; color: inherit; }
button { cursor: pointer; }
.shell { display: grid; grid-template-columns: 220px minmax(0, 1fr); max-width: 1080px; margin: 0 auto; padding: 2rem 1.5rem 7rem; gap: 2.5rem; }
.title { grid-column: 1 / -1; margin: 0; font-size: 22px; line-height: 1.4; }
.title small { display: block; font-size: 13px; font-weight: 400; color: var(--muted); }
/* ---------- 左の目次 ---------- */
.snav { position: sticky; top: 1.5rem; align-self: start; }
.snav ul { margin: 0; padding: 0; list-style: none; }
.snav a {
display: block; padding: 0.35rem 0.7rem;
color: var(--muted); text-decoration: none; border-radius: 7px;
}
.snav a:hover { color: var(--ink); background: var(--raised); }
.snav a[aria-current="true"] { color: var(--ink); font-weight: 600; background: var(--raised); }
.snav a.danger { color: var(--danger); }
/* ---------- 各節: 左に説明、右に入力 ---------- */
.sec {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
gap: 1.5rem 2.5rem;
padding: 1.8rem 0;
border-bottom: 1px solid var(--line);
}
.sec:first-child { padding-top: 0; }
.sec h2 { margin: 0 0 0.3rem; font-size: 15px; line-height: 1.5; }
.sec .desc { margin: 0; font-size: 13px; color: var(--muted); line-height: 1.8; }
.fields { display: grid; gap: 1.1rem; }
.field label { display: block; margin-bottom: 0.3rem; font-size: 13px; font-weight: 600; }
.field .hint { margin: 0.3rem 0 0; font-size: 12px; color: var(--quiet); }
.input, .select {
width: 100%; padding: 0.5rem 0.7rem;
background: var(--panel); border: 1px solid var(--line-2); border-radius: 8px; outline: 0;
}
.input:focus, .select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(124, 140, 255, 0.2); }
.prefix { display: flex; }
.prefix span {
padding: 0.5rem 0.7rem; color: var(--quiet); white-space: nowrap;
background: var(--raised); border: 1px solid var(--line-2); border-right: 0; border-radius: 8px 0 0 8px;
}
.prefix .input { border-radius: 0 8px 8px 0; }
.check { display: flex; gap: 0.6rem; align-items: flex-start; }
.check input { margin-top: 0.35rem; accent-color: var(--accent); }
.check b { display: block; font-size: 13px; }
.check span { font-size: 12px; color: var(--quiet); }
/* ---------- メンバー ---------- */
.members { width: 100%; border-collapse: collapse; }
.members td { padding: 0.6rem 0; border-bottom: 1px solid var(--line); vertical-align: middle; }
.members tr:last-child td { border-bottom: 0; }
.person { display: flex; align-items: center; gap: 0.6rem; }
.person i { display: grid; place-items: center; width: 30px; height: 30px; font-size: 12px; font-style: normal; font-weight: 700; color: var(--accent-ink); background: var(--accent); border-radius: 50%; }
.person b { display: block; font-size: 13px; line-height: 1.5; }
.person small { color: var(--quiet); }
.members .select { width: auto; padding: 0.3rem 0.5rem; }
.ghost { padding: 0.3rem 0.6rem; font-size: 12px; color: var(--muted); background: none; border: 1px solid var(--line-2); border-radius: 7px; }
.ghost:hover { color: var(--ink); border-color: var(--quiet); }
/* ---------- 危険な操作 ---------- */
.danger-box { padding: 1rem 1.1rem; background: var(--danger-soft); border: 1px solid rgba(240, 82, 90, 0.4); border-radius: 10px; }
.danger-box p { margin: 0 0 0.8rem; font-size: 13px; color: var(--muted); }
.danger-box b { color: var(--ink); }
.confirm { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.confirm .input { flex: 1; min-width: 180px; }
.btn-danger { padding: 0.5rem 0.9rem; font-weight: 600; color: #fff; background: var(--danger); border: 0; border-radius: 8px; }
.btn-danger:disabled { opacity: 0.35; cursor: not-allowed; }
/* ---------- 未保存の変更バー ---------- */
.savebar {
position: fixed; left: 50%; bottom: 1.2rem; z-index: 10;
display: flex; align-items: center; gap: 0.8rem;
width: min(640px, calc(100% - 2rem));
padding: 0.65rem 0.7rem 0.65rem 1.1rem;
background: var(--raised);
border: 1px solid var(--line-2);
border-radius: 12px;
box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.6);
translate: -50% 0;
transition: translate 0.2s ease, opacity 0.2s ease;
}
.savebar[hidden] { display: flex; translate: -50% 160%; opacity: 0; visibility: hidden; }
.savebar p { margin: 0; font-size: 13px; }
.savebar .dot { width: 8px; height: 8px; flex: none; background: #f5b83d; border-radius: 50%; }
.savebar .acts { display: flex; gap: 0.4rem; margin-left: auto; }
.btn { padding: 0.45rem 0.9rem; font-weight: 600; color: var(--accent-ink); background: var(--accent); border: 0; border-radius: 8px; }
.toast {
width: max-content; max-width: calc(100% - 2rem);
position: fixed; left: 50%; bottom: 1.2rem; z-index: 11; translate: -50% 0;
padding: 0.5rem 1rem; font-size: 13px; color: var(--accent-ink); background: #6ee7a8; border-radius: 99px;
}
.toast:empty { display: none; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (max-width: 860px) {
.shell { grid-template-columns: minmax(0, 1fr); gap: 1.2rem; }
.snav { position: static; overflow-x: auto; }
.snav ul { display: flex; gap: 0.3rem; }
.snav a { white-space: nowrap; }
.sec { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
}
@media (prefers-reduced-motion: reduce) { .savebar { transition: none; } }
.demo-note { position: relative; z-index: 6; max-width: 1100px; margin: 1rem auto; padding: 0.8rem 1rem; font-size: 13px; line-height: 1.8; letter-spacing: 0; border: 1px solid currentColor; }
:focus-visible { outline: 3px solid #1d4ed8; outline-offset: 3px; box-shadow: 0 0 0 6px #fff; }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
.plan:hover, .receipt:hover, .plan-cta:hover, .cta a:hover { transform: none; }
}
</style>
</head>
<body>
<div class="shell">
<h1 class="title">設定<small>Kakeru 請求 ・ ワークスペース</small></h1>
<nav class="snav" aria-label="設定の目次">
<ul>
<li><a href="#general" aria-current="true">一般</a></li>
<li><a href="#members">メンバー</a></li>
<li><a href="#notify">通知</a></li>
<li><a href="#danger" class="danger">危険な操作</a></li>
</ul>
</nav>
<form id="form" novalidate onsubmit="return false;">
<p class="desc">架空の設定画面です。保存はこのページ内だけで、再読み込みすると初期値に戻ります。招待・削除・外部への送信は行いません。</p>
<section class="sec" id="general" aria-labelledby="h-general">
<div>
<h2 id="h-general">一般</h2>
<p class="desc">ワークスペースの名前とURLです。URLを変えると、以前のURLのブックマークは使えなくなります。</p>
</div>
<div class="fields">
<div class="field">
<label for="ws-name">ワークスペース名</label>
<input class="input" id="ws-name" name="name" value="Kakeru 請求" autocomplete="organization" />
</div>
<div class="field">
<label for="ws-slug">URL</label>
<div class="prefix"><span>app.example.jp/</span><input class="input" id="ws-slug" name="slug" value="kakeru" aria-describedby="slug-hint" spellcheck="false" /></div>
<p class="hint" id="slug-hint">半角英数字とハイフンのみ。3〜32文字</p>
</div>
<div class="field">
<label for="tz">タイムゾーン</label>
<select class="select" id="tz" name="tz">
<option selected>(UTC+09:00) 東京</option>
<option>(UTC+00:00) 協定世界時</option>
<option>(UTC-08:00) ロサンゼルス</option>
</select>
</div>
</div>
</section>
<section class="sec" id="members" aria-labelledby="h-members">
<div>
<h2 id="h-members">メンバー</h2>
<p class="desc">役割によってできることが変わります。管理者は請求と設定を変更でき、編集者は請求書の作成と送付までです。</p>
</div>
<div>
<table class="members">
<caption class="sr">メンバーと役割</caption>
<tbody>
<tr>
<td><span class="person"><i>佐</i><span><b>佐藤 美咲</b><small>sato@example.jp</small></span></span></td>
<td><label class="sr" for="r1">佐藤 美咲 の役割</label><select class="select" id="r1" name="role-1"><option selected>管理者</option><option>編集者</option><option>閲覧のみ</option></select></td>
<td style="text-align:right"><span class="ghost" style="border:0">あなた</span></td>
</tr>
<tr>
<td><span class="person"><i style="background:#6ee7a8">林</i><span><b>林 健太</b><small>hayashi@example.jp</small></span></span></td>
<td><label class="sr" for="r2">林 健太 の役割</label><select class="select" id="r2" name="role-2"><option>管理者</option><option selected>編集者</option><option>閲覧のみ</option></select></td>
<td style="text-align:right"><button class="ghost" type="button" aria-label="林 健太 を外す">外す</button></td>
</tr>
<tr>
<td><span class="person"><i style="background:#f5b83d">森</i><span><b>森 由衣</b><small>mori@example.jp</small></span></span></td>
<td><label class="sr" for="r3">森 由衣 の役割</label><select class="select" id="r3" name="role-3"><option>管理者</option><option>編集者</option><option selected>閲覧のみ</option></select></td>
<td style="text-align:right"><button class="ghost" type="button" aria-label="森 由衣 を外す">外す</button></td>
</tr>
</tbody>
</table>
<p style="margin:0.8rem 0 0"><button class="ghost" type="button">+ メンバーを招待</button></p>
</div>
</section>
<section class="sec" id="notify" aria-labelledby="h-notify">
<div>
<h2 id="h-notify">通知</h2>
<p class="desc">メールで知らせる出来事を選びます。個人ごとの設定は、各自のアカウント画面で変えられます。</p>
</div>
<div class="fields">
<label class="check"><input type="checkbox" name="n-paid" checked /><span><b>入金を照合したとき</b><span>取引先ごとに1通。まとめて届くことはありません</span></span></label>
<label class="check"><input type="checkbox" name="n-due" checked /><span><b>支払期限の3日前</b><span>未入金の請求書だけが対象です</span></span></label>
<label class="check"><input type="checkbox" name="n-weekly" /><span><b>毎週月曜の集計</b><span>前の週の請求額・入金額・未回収</span></span></label>
</div>
</section>
<section class="sec" id="danger" aria-labelledby="h-danger">
<div>
<h2 id="h-danger">危険な操作</h2>
<p class="desc">取り消せない操作です。実行する前に、請求書のデータを書き出しておいてください。</p>
</div>
<div class="danger-box">
<p>ワークスペースを削除すると、<b>請求書128件・取引先46件</b>を含むすべてのデータが消え、元に戻せません。</p>
<label for="confirm-name" style="display:block;margin-bottom:0.35rem;font-size:13px;font-weight:600">確認のため <b>Kakeru 請求</b> と入力してください</label>
<div class="confirm">
<input class="input" id="confirm-name" autocomplete="off" spellcheck="false" />
<button class="btn-danger" id="delete" type="button" disabled>ワークスペースを削除</button>
</div>
</div>
</section>
</form>
</div>
<div class="savebar" id="savebar" role="region" aria-label="未保存の変更" hidden>
<span class="dot" aria-hidden="true"></span>
<p><span id="dirtyCount">1</span>件の未保存の変更があります</p>
<div class="acts">
<button class="ghost" type="button" id="reset">取り消す</button>
<button class="btn" type="button" id="save">変更を保存</button>
</div>
</div>
<p class="toast" id="toast" role="status" aria-live="polite"></p>
<script>
const form = document.getElementById("form");
const bar = document.getElementById("savebar");
const dirtyCount = document.getElementById("dirtyCount");
const toast = document.getElementById("toast");
form.addEventListener("submit", event => event.preventDefault());
// 保存済みの値を覚えておき、いまの値と比べて「変わった項目の数」を出す。
// 一度触ったら未保存、とすると、元に戻しても保存バーが消えない
const fields = [...form.querySelectorAll("input[name], select[name]")];
const valueOf = (el) => (el.type === "checkbox" ? el.checked : el.value);
let saved = new Map(fields.map((el) => [el, valueOf(el)]));
function refresh() {
const n = fields.filter((el) => valueOf(el) !== saved.get(el)).length;
dirtyCount.textContent = n;
bar.hidden = n === 0;
}
form.addEventListener("input", refresh);
form.addEventListener("change", refresh);
document.getElementById("reset").addEventListener("click", () => {
for (const el of fields) {
if (el.type === "checkbox") el.checked = saved.get(el);
else el.value = saved.get(el);
}
refresh();
});
document.getElementById("save").addEventListener("click", () => {
saved = new Map(fields.map((el) => [el, valueOf(el)]));
refresh();
toast.textContent = "デモ:このページ内に仮保存しました。再読み込みすると元に戻ります。";
setTimeout(() => (toast.textContent = ""), 1800);
});
// 未保存のまま離れようとしたら止める
addEventListener("beforeunload", (e) => {
if (!bar.hidden) e.preventDefault();
});
// 取り消せない操作は、名前を打ち込むまで押せない
const confirmInput = document.getElementById("confirm-name");
const del = document.getElementById("delete");
confirmInput.addEventListener("input", () => {
del.disabled = confirmInput.value.trim() !== "Kakeru 請求";
});
del.addEventListener("click", () => {
toast.textContent = "デモ:削除は行っていません。";
});
form.querySelectorAll('.members button, #members > div button').forEach(button => {
button.addEventListener("click", () => {
toast.textContent = "デモ:メンバーの招待・削除は行っていません。";
});
});
// 見本として、開いた時点でワークスペース名を1か所書き換えた状態にしておく
document.getElementById("ws-name").value = "Kakeru 請求 本社";
refresh();
// 目次の現在地をスクロールに合わせる
const links = [...document.querySelectorAll(".snav a")];
const io = new IntersectionObserver((entries) => {
for (const en of entries) {
if (!en.isIntersecting) continue;
links.forEach((a) => a.setAttribute("aria-current", String(a.hash === "#" + en.target.id)));
}
}, { rootMargin: "-20% 0px -70% 0px" });
document.querySelectorAll(".sec").forEach((s) => io.observe(s));
</script>
</body>
</html>
2026年10月14日、Chromiumで320・390・768・1280pxの描画とキーボードのフォーカス表示を確認しました。他のブラウザは未確認です。利用先のブラウザと実データでも確認してください。
概要
設定画面は、SaaSの中でいちばん慎重に触られる場所です。請求の宛先を変える、メンバーの権限を変える、場合によってはワークスペースそのものを消す。どれも間違えたときの損害が大きい操作で、利用者は常に「本当に大丈夫か」と構えています。この画面の設計は、その不安を1つずつ潰していく作業です。
何が変わったかを伝え続ける
設定を変えたのに保存を押し忘れて画面を離れる。これは設定画面で最も多い事故です。変更があることを画面の下に浮かせて常に見せ、離れようとしたら止めます。
崩れる形1か所でも触ったら未保存にする
入力を元に戻しても未保存のまま残るので、何が変わったのか分からなくなります。結局、全部を見直してから保存することになります。
効く形保存済みの値と比べて数える
変わった項目の数を出し、元に戻せば減り、0になれば消えます。いま保存すると何件変わるのかが常に分かる状態です。
説明は入力欄の外に出す
各節を2列に分け、左に「この設定を変えると何が起きるか」、右に入力欄を置きます。URLを変えると以前のブックマークが使えなくなる、役割によってできることが変わる。こうした結果は、入力欄の下に小さく書いても読まれません。
取り消せない操作は、手を止めさせる
削除のような取り消せない操作は、消えるものの件数を具体的に示し、名前を打ち込むまで押せなくする。「はい」を押すだけの確認は、読まれずに押されます。
この仕組みは強い抵抗を生むので、本当に取り消せない操作にだけ使います。メンバーを外す程度の操作にまで使うと、確認そのものが読み飛ばされるようになります。
ダークテーマでは、入力欄と地の明度差が小さくなりがちです。入力欄は地より一段明るい面にし、フォーカスしたときは枠の色と薄い光の輪の2つで示すと、どこに入力しているかを見失いません。
フォームの構成要素
使いどころ
- SaaSのワークスペース・組織・アカウントの設定
- 社内管理ツールの環境設定
- 権限と招待を扱うメンバー管理
- 削除や解約など取り消せない操作を含む画面
このデザインの分類
背景と狙いどころ
よくある質問
1か所触っただけで未保存扱いにしてはだめ?
削除の確認はダイアログではだめ?
説明を左、入力を右にするのはなぜ?
この画面を実装する
動くサンプルとコピペできるコードを、技術別にまとめています。
関連するデザイン
設計図面風の見積もり依頼フォーム
青焼き図面の方眼と表題欄をそのまま画面に持ち込んだ見積もりフォーム。入力欄の右に工程と前提条件を並べ、送信前に何が起きるかを見せて心理的な壁を下げます。
スイス様式のシンプルなお問い合わせフォーム
厳格なグリッドと無装飾サンセリフで構築する、スイス国際タイポグラフィ様式のお問い合わせフォーム。色を排し、罫線・余白・タイポの階層だけで「真剣さ」を伝える設計。
1画面を大きく見せる資産管理アプリの紹介
端末を1台だけ大きく置き、周囲にUIの断片を浮かせるアプリ紹介セクション。数字そのものを主役にする構図で、家計簿・投資・ヘルスケアなど「自分の数値」を扱うアプリに向く設計。