Onboarding — Sample-data / try with demo prompt
A first-run onboarding pattern that turns an empty workspace into something you can actually click. A Start with sample data prompt offers two choice cards — load a faux CRM pipeline of example deals, or start from scratch and stay empty. Picking the demo shows a brief shimmering skeleton before the populated table animates in, with a toast confirming the count and a Clear demo data button to revert. Toggle between a modal prompt and an inline banner, and between preview-thumbnail and text-only cards.
MCP
代码
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-2: 0 8px 24px rgba(16, 19, 34, 0.08);
--sh-lift: 0 18px 48px rgba(16, 19, 34, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
font-size: 15px;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
p {
margin: 0;
}
button {
font-family: inherit;
}
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: var(--r-sm);
}
.page {
max-width: 1080px;
margin: 0 auto;
padding: 40px 24px 64px;
}
/* ---------- Masthead ---------- */
.masthead {
margin-bottom: 26px;
}
.kicker {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--brand);
margin-bottom: 8px;
}
.title {
font-size: clamp(26px, 4vw, 36px);
font-weight: 800;
letter-spacing: -0.02em;
color: var(--ink);
}
.lede {
margin-top: 12px;
max-width: 64ch;
color: var(--ink-2);
font-size: 15px;
}
/* ---------- Variant switcher ---------- */
.switcher {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 22px;
}
.seg {
display: inline-flex;
padding: 4px;
gap: 2px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--sh-1);
}
.seg-btn {
border: 0;
background: transparent;
color: var(--muted);
font-size: 13px;
font-weight: 600;
padding: 7px 14px;
border-radius: 999px;
cursor: pointer;
transition: background 0.16s ease, color 0.16s ease;
}
.seg-btn:hover {
color: var(--ink-2);
}
.seg-btn.is-active {
background: var(--brand);
color: #fff;
box-shadow: 0 2px 8px rgba(91, 91, 240, 0.35);
}
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 268px;
gap: 22px;
align-items: start;
}
/* ---------- App shell ---------- */
.app {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
overflow: hidden;
}
.app-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 18px;
border-bottom: 1px solid var(--line);
background: linear-gradient(180deg, #fff, #fbfbff);
}
.app-brand {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.app-logo {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 9px;
background: var(--brand-50);
color: var(--brand-d);
flex: none;
}
.app-name {
font-weight: 700;
font-size: 14px;
color: var(--ink);
white-space: nowrap;
}
.app-crumb {
font-size: 13px;
color: var(--muted);
white-space: nowrap;
}
.app-tools {
display: flex;
align-items: center;
gap: 10px;
}
.rows-badge {
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
padding: 5px 10px;
border-radius: 999px;
white-space: nowrap;
}
.ghost-btn {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--line-2);
background: var(--white);
color: var(--ink-2);
font-size: 13px;
font-weight: 600;
padding: 6px 11px;
border-radius: var(--r-sm);
cursor: pointer;
transition: border-color 0.16s, color 0.16s, background 0.16s;
}
.ghost-btn:hover {
border-color: var(--danger);
color: var(--danger);
background: #fdf3f1;
}
.ghost-btn:active {
transform: translateY(1px);
}
/* ---------- Inline banner variant ---------- */
.banner {
display: flex;
align-items: center;
gap: 14px;
position: relative;
margin: 16px 18px 0;
padding: 16px 44px 16px 16px;
background: linear-gradient(120deg, var(--brand-50), #f3f7ff 60%, var(--accent-soft));
border: 1px solid rgba(91, 91, 240, 0.22);
border-radius: var(--r-md);
animation: drop 0.32s cubic-bezier(0.2, 0.9, 0.3, 1.1) both;
}
.banner-spark {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border-radius: 12px;
background: #fff;
color: var(--brand);
box-shadow: var(--sh-1);
flex: none;
}
.banner-copy {
min-width: 0;
flex: 1;
}
.banner-title {
font-weight: 700;
font-size: 14px;
color: var(--ink);
}
.banner-sub {
font-size: 13px;
color: var(--ink-2);
margin-top: 2px;
}
.banner-actions {
display: flex;
gap: 8px;
flex: none;
}
.banner-x {
position: absolute;
top: 10px;
right: 10px;
width: 26px;
height: 26px;
display: grid;
place-items: center;
border: 0;
border-radius: 8px;
background: transparent;
color: var(--muted);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.banner-x:hover {
background: rgba(16, 19, 34, 0.06);
color: var(--ink);
}
/* ---------- Buttons ---------- */
.btn {
border: 1px solid transparent;
font-size: 13px;
font-weight: 600;
padding: 8px 14px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.16s, border-color 0.16s, transform 0.06s, box-shadow 0.16s;
white-space: nowrap;
}
.btn-primary {
background: var(--brand);
color: #fff;
box-shadow: 0 2px 8px rgba(91, 91, 240, 0.32);
}
.btn-primary:hover {
background: var(--brand-d);
}
.btn-primary:active {
transform: translateY(1px);
}
.btn-ghost {
background: var(--white);
border-color: var(--line-2);
color: var(--ink-2);
}
.btn-ghost:hover {
border-color: var(--brand);
color: var(--brand-d);
background: var(--brand-50);
}
.btn-ghost:active {
transform: translateY(1px);
}
/* ---------- Board / table ---------- */
.board {
padding: 16px 18px 18px;
}
.board-head,
.row {
display: grid;
grid-template-columns: minmax(0, 2.1fr) minmax(0, 1.6fr) minmax(0, 1.4fr) minmax(0, 1.2fr) minmax(0, 0.9fr);
gap: 12px;
align-items: center;
}
.board-head {
padding: 0 12px 10px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
border-bottom: 1px solid var(--line);
}
.board-head .num,
.row .num {
text-align: right;
}
/* Empty state */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 46px 16px 40px;
color: var(--muted);
}
.empty-art {
color: var(--brand);
margin-bottom: 12px;
}
.empty-title {
font-weight: 700;
font-size: 15px;
color: var(--ink-2);
}
.empty-sub {
font-size: 13px;
margin-top: 3px;
}
/* Skeleton */
.skeleton {
padding: 8px 0 2px;
}
.sk-row {
height: 46px;
margin: 6px 12px;
border-radius: var(--r-sm);
background: linear-gradient(
90deg,
rgba(16, 19, 34, 0.05) 25%,
rgba(16, 19, 34, 0.1) 37%,
rgba(16, 19, 34, 0.05) 63%
);
background-size: 400% 100%;
animation: shimmer 1.2s ease-in-out infinite;
}
.sk-row:nth-child(2) {
animation-delay: 0.08s;
}
.sk-row:nth-child(3) {
animation-delay: 0.16s;
}
.sk-row:nth-child(4) {
animation-delay: 0.24s;
}
.sk-row:nth-child(5) {
animation-delay: 0.32s;
}
@keyframes shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: 0 0;
}
}
/* Populated rows */
.rows {
display: flex;
flex-direction: column;
}
.row {
padding: 11px 12px;
border-radius: var(--r-sm);
font-size: 13.5px;
border: 1px solid transparent;
transition: background 0.14s, border-color 0.14s;
animation: rowIn 0.34s cubic-bezier(0.2, 0.9, 0.3, 1.05) both;
}
.row:hover {
background: var(--bg);
border-color: var(--line);
}
.row + .row {
margin-top: 2px;
}
.deal-cell {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.deal-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex: none;
background: var(--brand);
}
.deal-name {
font-weight: 600;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cell-muted {
color: var(--ink-2);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.owner-cell {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.avatar {
width: 24px;
height: 24px;
border-radius: 50%;
flex: none;
display: grid;
place-items: center;
font-size: 11px;
font-weight: 700;
color: #fff;
}
.value-cell {
text-align: right;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--ink);
}
/* Stage chips */
.chip {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11.5px;
font-weight: 600;
padding: 3px 9px;
border-radius: 999px;
white-space: nowrap;
}
.chip::before {
content: "";
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.chip-lead {
background: var(--brand-50);
color: var(--brand-700);
}
.chip-qual {
background: var(--accent-soft);
color: #0a7d73;
}
.chip-prop {
background: #fff3e2;
color: var(--warn);
}
.chip-won {
background: #e4f6ec;
color: var(--ok);
}
.chip-lost {
background: #fdeae7;
color: var(--danger);
}
/* ---------- Modal ---------- */
.overlay {
position: fixed;
inset: 0;
display: grid;
place-items: center;
padding: 20px;
z-index: 50;
}
.backdrop {
position: absolute;
inset: 0;
background: rgba(16, 19, 34, 0.46);
backdrop-filter: blur(2px);
animation: fade 0.2s ease both;
}
.modal {
position: relative;
width: min(560px, 100%);
background: var(--surface);
border-radius: var(--r-lg);
box-shadow: var(--sh-lift);
padding: 28px 26px 22px;
animation: pop 0.26s cubic-bezier(0.2, 0.9, 0.3, 1.08) both;
}
.modal-x {
position: absolute;
top: 14px;
right: 14px;
width: 32px;
height: 32px;
display: grid;
place-items: center;
border: 0;
border-radius: 9px;
background: transparent;
color: var(--muted);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.modal-x:hover {
background: rgba(16, 19, 34, 0.06);
color: var(--ink);
}
.modal-head {
text-align: center;
margin-bottom: 20px;
}
.modal-spark {
display: inline-grid;
place-items: center;
width: 50px;
height: 50px;
border-radius: 15px;
background: linear-gradient(135deg, var(--brand-50), var(--accent-soft));
color: var(--brand);
margin-bottom: 12px;
}
.modal-head h2 {
font-size: 21px;
font-weight: 800;
letter-spacing: -0.01em;
}
.modal-head p {
margin-top: 7px;
font-size: 14px;
color: var(--ink-2);
max-width: 42ch;
margin-inline: auto;
}
/* ---------- Choice cards ---------- */
.choices {
display: grid;
gap: 12px;
}
.choice {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
text-align: left;
background: var(--white);
border: 1.5px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
cursor: pointer;
transition: border-color 0.16s, box-shadow 0.16s, transform 0.08s, background 0.16s;
}
.choice:hover {
border-color: var(--brand);
box-shadow: 0 6px 18px rgba(91, 91, 240, 0.16);
transform: translateY(-1px);
}
.choice:active {
transform: translateY(0);
}
.choice-demo:hover {
background: linear-gradient(180deg, #fff, var(--brand-50));
}
.choice-thumb {
flex: none;
width: 76px;
height: 60px;
border-radius: 10px;
background: linear-gradient(160deg, #eef0ff, #f7f8ff);
border: 1px solid var(--line);
padding: 9px;
display: flex;
flex-direction: column;
gap: 5px;
justify-content: center;
position: relative;
overflow: hidden;
}
.thumb-bar {
height: 5px;
border-radius: 3px;
background: rgba(91, 91, 240, 0.35);
width: 100%;
}
.thumb-bar.w2 {
width: 70%;
}
.thumb-bar.w3 {
width: 85%;
}
.thumb-pill {
position: absolute;
right: 8px;
bottom: 8px;
width: 18px;
height: 9px;
border-radius: 999px;
background: var(--accent);
}
.thumb-empty {
background: repeating-linear-gradient(
45deg,
#f6f7fb,
#f6f7fb 6px,
#eef0f6 6px,
#eef0f6 12px
);
display: grid;
place-items: center;
}
.thumb-plus {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 9px;
background: #fff;
border: 1px solid var(--line);
color: var(--muted);
}
.choice-body {
flex: 1;
min-width: 0;
}
.choice-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 700;
font-size: 15px;
color: var(--ink);
}
.choice-tag {
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--accent);
background: var(--accent-soft);
padding: 2px 7px;
border-radius: 999px;
}
.choice-sub {
display: block;
margin-top: 3px;
font-size: 13px;
color: var(--ink-2);
line-height: 1.45;
}
.choice-go {
flex: none;
color: var(--muted);
display: grid;
place-items: center;
transition: color 0.16s, transform 0.16s;
}
.choice:hover .choice-go {
color: var(--brand);
transform: translateX(2px);
}
/* Text-only card variant */
.cards-text .choice-thumb {
display: none;
}
.modal-foot {
margin-top: 16px;
text-align: center;
font-size: 12px;
color: var(--muted);
}
/* ---------- Side notes ---------- */
.notes {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
padding: 18px;
}
.notes-h {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 12px;
}
.note-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 11px;
}
.note-list li {
position: relative;
padding-left: 20px;
font-size: 13px;
color: var(--ink-2);
line-height: 1.5;
}
.note-list li::before {
content: "";
position: absolute;
left: 2px;
top: 7px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--brand);
}
.note-list strong {
color: var(--ink);
}
.note-list code {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 12px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 5px;
padding: 1px 5px;
}
.note-foot {
margin-top: 16px;
padding-top: 14px;
border-top: 1px solid var(--line);
font-size: 12px;
color: var(--muted);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 18px);
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 600;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--sh-lift);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 60;
}
.toast.is-visible {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Animations ---------- */
@keyframes fade {
from {
opacity: 0;
}
}
@keyframes pop {
from {
opacity: 0;
transform: translateY(10px) scale(0.97);
}
}
@keyframes drop {
from {
opacity: 0;
transform: translateY(-8px);
}
}
@keyframes rowIn {
from {
opacity: 0;
transform: translateY(7px);
}
}
/* ---------- Responsive ---------- */
@media (max-width: 880px) {
.layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 520px) {
.page {
padding: 28px 14px 52px;
}
.switcher {
gap: 8px;
}
.seg {
width: 100%;
justify-content: stretch;
}
.seg-btn {
flex: 1;
text-align: center;
padding: 8px 8px;
}
.app-crumb {
display: none;
}
.banner {
flex-wrap: wrap;
padding: 14px 40px 14px 14px;
}
.banner-actions {
width: 100%;
}
.banner-actions .btn {
flex: 1;
}
/* Collapse table to label/value pairs on phones */
.board-head {
display: none;
}
.row {
grid-template-columns: 1fr auto;
grid-template-areas:
"deal value"
"meta meta";
gap: 6px 12px;
border: 1px solid var(--line);
background: var(--surface);
padding: 12px;
}
.row + .row {
margin-top: 8px;
}
.deal-cell {
grid-area: deal;
}
.value-cell {
grid-area: value;
}
.row > .cell-company,
.row > .owner-cell,
.row > .cell-stage {
grid-area: meta;
}
.cell-company {
display: none;
}
.owner-cell {
grid-area: meta;
}
.cell-stage {
grid-area: meta;
justify-self: start;
}
.modal {
padding: 24px 18px 18px;
}
.choice {
gap: 12px;
padding: 12px;
}
.choice-thumb {
width: 60px;
height: 52px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
var overlay = document.getElementById("overlay");
var modal = document.getElementById("modal");
var banner = document.getElementById("banner");
var emptyState = document.getElementById("empty-state");
var skeleton = document.getElementById("skeleton");
var rowsEl = document.getElementById("rows");
var rowsBadge = document.getElementById("rows-badge");
var clearBtn = document.getElementById("clear-btn");
var choices = document.getElementById("choices");
var toastEl = document.getElementById("toast");
var variantBtns = Array.prototype.slice.call(
document.querySelectorAll(".seg-btn[data-variant]")
);
var cardBtns = Array.prototype.slice.call(
document.querySelectorAll(".seg-btn[data-cards]")
);
// ----- Fictional sample dataset -----
var DEMO = [
{ deal: "Q3 Platform Upgrade", company: "Northwind Bakeries", owner: "Priya Nandakumar", stage: "proposal", value: 48000, hue: 248 },
{ deal: "Annual Renewal — Pro", company: "Halcyon Devices", owner: "Marcus Tilden", stage: "won", value: 31500, hue: 168 },
{ deal: "Pilot: Field Sync", company: "Cedar & Vale Logistics", owner: "Aisha Okonkwo", stage: "qualified", value: 12200, hue: 22 },
{ deal: "Seat Expansion (+40)", company: "Brightloom Studio", owner: "Devon Park", stage: "lead", value: 9800, hue: 320 },
{ deal: "Migration & Onboarding", company: "Tess Maritime", owner: "Priya Nandakumar", stage: "proposal", value: 27400, hue: 248 },
{ deal: "Security Add-on", company: "Quillstone Legal", owner: "Marcus Tilden", stage: "qualified", value: 15600, hue: 168 },
{ deal: "Replacement Tooling", company: "Oakmere Foundry", owner: "Aisha Okonkwo", stage: "lost", value: 22000, hue: 22 },
{ deal: "New Region Rollout", company: "Solane Health Group", owner: "Devon Park", stage: "lead", value: 64500, hue: 320 }
];
var STAGE = {
lead: { label: "Lead", cls: "chip-lead" },
qualified: { label: "Qualified", cls: "chip-qual" },
proposal: { label: "Proposal", cls: "chip-prop" },
won: { label: "Won", cls: "chip-won" },
lost: { label: "Lost", cls: "chip-lost" }
};
var state = {
variant: "modal", // "modal" | "banner"
cards: "thumb", // "thumb" | "text"
populated: false,
loading: false
};
var skeletonTimer = null;
var lastFocus = null;
// ----- Toast helper -----
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 2200);
}
function fmt(n) {
return "$" + n.toLocaleString("en-US");
}
function initials(name) {
return name
.split(" ")
.map(function (p) {
return p.charAt(0);
})
.join("")
.slice(0, 2)
.toUpperCase();
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
// ----- Build a populated row -----
function rowMarkup(d, i) {
var s = STAGE[d.stage];
return (
'<div class="row" role="row" style="animation-delay:' + i * 45 + 'ms">' +
'<div class="deal-cell" role="cell">' +
'<span class="deal-dot" style="background:hsl(' + d.hue + ' 78% 60%)"></span>' +
'<span class="deal-name">' + escapeHtml(d.deal) + "</span>" +
"</div>" +
'<div class="cell-muted cell-company" role="cell">' + escapeHtml(d.company) + "</div>" +
'<div class="owner-cell cell-owner" role="cell">' +
'<span class="avatar" style="background:hsl(' + d.hue + ' 64% 52%)">' + initials(d.owner) + "</span>" +
'<span class="cell-muted">' + escapeHtml(d.owner) + "</span>" +
"</div>" +
'<div class="cell-stage" role="cell"><span class="chip ' + s.cls + '">' + s.label + "</span></div>" +
'<div class="value-cell num" role="cell">' + fmt(d.value) + "</div>" +
"</div>"
);
}
function updateBadge() {
var n = state.populated ? DEMO.length : 0;
rowsBadge.textContent = n + (n === 1 ? " deal" : " deals");
}
// ----- State transitions -----
function showEmpty() {
state.populated = false;
state.loading = false;
if (skeletonTimer) {
clearTimeout(skeletonTimer);
skeletonTimer = null;
}
skeleton.hidden = true;
rowsEl.innerHTML = "";
emptyState.hidden = false;
clearBtn.hidden = true;
updateBadge();
}
function populate() {
state.populated = true;
state.loading = false;
skeleton.hidden = true;
emptyState.hidden = true;
rowsEl.innerHTML = DEMO.map(rowMarkup).join("");
clearBtn.hidden = false;
updateBadge();
}
function loadDemo() {
if (state.loading || state.populated) return;
closePrompt();
// brief skeleton, then populated state
state.loading = true;
emptyState.hidden = true;
rowsEl.innerHTML = "";
skeleton.hidden = false;
if (skeletonTimer) clearTimeout(skeletonTimer);
skeletonTimer = setTimeout(function () {
populate();
toast("Loaded 8 example deals — explore away");
}, 950);
}
function startScratch() {
closePrompt();
// workspace stays empty; nothing else to do
}
function clearDemo() {
showEmpty();
toast("Demo data cleared");
// bring the prompt back so it can be re-tried
openPrompt();
}
// ----- Prompt (modal or banner) -----
function openPrompt() {
if (state.populated) return;
if (state.variant === "modal") {
lastFocus = document.activeElement;
overlay.hidden = false;
banner.hidden = true;
// focus first choice for keyboard users
var first = modal.querySelector(".choice");
if (first) first.focus();
} else {
banner.hidden = false;
overlay.hidden = true;
}
}
function closePrompt() {
overlay.hidden = true;
banner.hidden = true;
if (lastFocus && typeof lastFocus.focus === "function") {
lastFocus.focus();
lastFocus = null;
}
}
// Re-render the prompt for the current variant if it's currently meant to be open
function refreshPrompt() {
var promptOpen = !overlay.hidden || !banner.hidden;
if (state.populated) {
overlay.hidden = true;
banner.hidden = true;
return;
}
if (promptOpen) {
openPrompt();
}
}
// ----- Action delegation (works in modal + banner) -----
function handleAction(action) {
if (action === "load-demo") loadDemo();
else if (action === "scratch") startScratch();
}
document.addEventListener("click", function (e) {
var el = e.target.closest("[data-action]");
if (el) {
e.preventDefault();
handleAction(el.getAttribute("data-action"));
}
});
clearBtn.addEventListener("click", clearDemo);
// Esc closes the modal overlay (treated as "start from scratch")
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !overlay.hidden) {
e.preventDefault();
startScratch();
}
});
// Focus trap inside the modal
overlay.addEventListener("keydown", function (e) {
if (e.key !== "Tab" || overlay.hidden) return;
var focusables = modal.querySelectorAll(
'button, [href], [tabindex]:not([tabindex="-1"])'
);
if (!focusables.length) return;
var first = focusables[0];
var last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
// ----- Variant switchers -----
function setActive(btns, attr, val) {
btns.forEach(function (b) {
var on = b.getAttribute(attr) === val;
b.classList.toggle("is-active", on);
b.setAttribute("aria-checked", on ? "true" : "false");
});
}
variantBtns.forEach(function (b) {
b.addEventListener("click", function () {
state.variant = b.getAttribute("data-variant");
setActive(variantBtns, "data-variant", state.variant);
refreshPrompt();
});
});
cardBtns.forEach(function (b) {
b.addEventListener("click", function () {
state.cards = b.getAttribute("data-cards");
setActive(cardBtns, "data-cards", state.cards);
choices.classList.toggle("cards-text", state.cards === "text");
});
});
// ----- Init -----
showEmpty();
openPrompt();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Onboarding — Sample-data / try with demo prompt</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<p class="kicker">Onboarding pattern</p>
<h1 class="title">Start with sample data?</h1>
<p class="lede">
When a workspace is empty, a “try with demo” prompt lets
people explore a populated UI before committing real work. Switch
between a modal prompt and an inline banner, and toggle whether the
choice cards show a preview thumbnail or text only.
</p>
<div class="switcher" role="group" aria-label="Demo variants">
<div class="seg" role="radiogroup" aria-label="Prompt style">
<button
class="seg-btn is-active"
type="button"
role="radio"
aria-checked="true"
data-variant="modal"
>
Modal prompt
</button>
<button
class="seg-btn"
type="button"
role="radio"
aria-checked="false"
data-variant="banner"
>
Inline banner
</button>
</div>
<div class="seg" role="radiogroup" aria-label="Card style">
<button
class="seg-btn is-active"
type="button"
role="radio"
aria-checked="true"
data-cards="thumb"
>
Preview thumbnail
</button>
<button
class="seg-btn"
type="button"
role="radio"
aria-checked="false"
data-cards="text"
>
Text only
</button>
</div>
</div>
</header>
<main class="layout">
<!-- Faux application shell -->
<section class="app" aria-label="Sample workspace application">
<div class="app-bar">
<div class="app-brand">
<span class="app-logo" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path
d="M4 6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5v11a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 4 17.5v-11Z"
fill="currentColor"
opacity=".18"
/>
<path
d="M8 9h8M8 12.5h8M8 16h5"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
/>
</svg>
</span>
<span class="app-name">Tarn CRM</span>
<span class="app-crumb">/ Deals</span>
</div>
<div class="app-tools">
<span class="rows-badge" id="rows-badge" aria-live="polite">0 deals</span>
<button class="ghost-btn" type="button" id="clear-btn" hidden>
<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true">
<path
d="M6 7h12M9 7V5.5A1.5 1.5 0 0 1 10.5 4h3A1.5 1.5 0 0 1 15 5.5V7m2 0-.6 11a2 2 0 0 1-2 1.9H9.6a2 2 0 0 1-2-1.9L7 7"
stroke="currentColor"
stroke-width="1.6"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
/>
</svg>
Clear demo data
</button>
</div>
</div>
<!-- Inline banner variant lives here, above the table -->
<div class="banner" id="banner" role="region" aria-label="Sample data prompt" hidden>
<span class="banner-spark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20">
<path
d="m12 3 1.9 4.6L18.5 9l-4.6 1.4L12 15l-1.9-4.6L5.5 9l4.6-1.4L12 3Z"
fill="currentColor"
/>
<path d="m18 14 .9 2.3L21 17l-2.1.7L18 20l-.9-2.3L15 17l2.1-.7L18 14Z" fill="currentColor" opacity=".7" />
</svg>
</span>
<div class="banner-copy">
<p class="banner-title">This workspace is empty</p>
<p class="banner-sub">
Load a set of example deals to explore the board, or start
clean and add your own.
</p>
</div>
<div class="banner-actions">
<button class="btn btn-primary" type="button" data-action="load-demo">
Load demo data
</button>
<button class="btn btn-ghost" type="button" data-action="scratch">
Start from scratch
</button>
</div>
<button class="banner-x" type="button" data-action="scratch" aria-label="Dismiss prompt">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path d="M6 6l12 12M18 6 6 18" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
</button>
</div>
<!-- The table itself: empty / skeleton / populated states -->
<div class="board">
<div class="board-head" role="row">
<span role="columnheader">Deal</span>
<span role="columnheader">Company</span>
<span role="columnheader">Owner</span>
<span role="columnheader">Stage</span>
<span role="columnheader" class="num">Value</span>
</div>
<!-- Empty state -->
<div class="empty-state" id="empty-state">
<span class="empty-art" aria-hidden="true">
<svg viewBox="0 0 64 64" width="56" height="56">
<rect x="10" y="14" width="44" height="36" rx="6" fill="currentColor" opacity=".1" />
<path d="M18 26h28M18 33h28M18 40h18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" opacity=".4" />
<circle cx="48" cy="44" r="11" fill="var(--surface)" />
<circle cx="48" cy="44" r="9.5" fill="none" stroke="currentColor" stroke-width="2" />
<path d="M48 40v8M44 44h8" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</span>
<p class="empty-title">No deals yet</p>
<p class="empty-sub">Your pipeline will appear here once you add deals.</p>
</div>
<!-- Skeleton (loading demo) -->
<div class="skeleton" id="skeleton" hidden aria-hidden="true">
<div class="sk-row"></div>
<div class="sk-row"></div>
<div class="sk-row"></div>
<div class="sk-row"></div>
<div class="sk-row"></div>
</div>
<!-- Populated rows injected here -->
<div class="rows" id="rows" role="rowgroup"></div>
</div>
</section>
<aside class="notes" aria-label="Pattern notes">
<h2 class="notes-h">How it behaves</h2>
<ul class="note-list">
<li><strong>Load demo data</strong> shows a brief skeleton, then fills the board with example deals and fires a toast.</li>
<li><strong>Start from scratch</strong> dismisses the prompt and leaves the workspace empty.</li>
<li><strong>Clear demo data</strong> reverts to the empty state and brings the prompt back.</li>
<li>The row counter is an <code>aria-live</code> region so the count is announced.</li>
</ul>
<p class="note-foot">Fictional companies & figures — illustrative UI only.</p>
</aside>
</main>
</div>
<!-- Modal prompt variant -->
<div class="overlay" id="overlay" hidden>
<div class="backdrop" data-action="scratch"></div>
<div
class="modal"
id="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
>
<button class="modal-x" type="button" data-action="scratch" aria-label="Close">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path d="M6 6l12 12M18 6 6 18" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
</svg>
</button>
<div class="modal-head">
<span class="modal-spark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22">
<path d="m12 3 1.9 4.6L18.5 9l-4.6 1.4L12 15l-1.9-4.6L5.5 9l4.6-1.4L12 3Z" fill="currentColor" />
<path d="m18 14 .9 2.3L21 17l-2.1.7L18 20l-.9-2.3L15 17l2.1-.7L18 14Z" fill="currentColor" opacity=".7" />
</svg>
</span>
<h2 id="modal-title">Start with sample data?</h2>
<p id="modal-desc">
Explore Tarn CRM with a ready-made pipeline, or begin with a blank
workspace. You can clear the demo data at any time.
</p>
</div>
<div class="choices" id="choices">
<button class="choice choice-demo" type="button" data-action="load-demo">
<span class="choice-thumb" aria-hidden="true">
<span class="thumb-bar"></span>
<span class="thumb-bar w2"></span>
<span class="thumb-bar w3"></span>
<span class="thumb-bar"></span>
<span class="thumb-pill"></span>
</span>
<span class="choice-body">
<span class="choice-title">
Load demo data
<span class="choice-tag">Recommended</span>
</span>
<span class="choice-sub">8 example deals across every stage so you can click around right away.</span>
</span>
<span class="choice-go" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" /></svg>
</span>
</button>
<button class="choice choice-scratch" type="button" data-action="scratch">
<span class="choice-thumb thumb-empty" aria-hidden="true">
<span class="thumb-plus">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 6v12M6 12h12" stroke="currentColor" stroke-width="2" stroke-linecap="round" /></svg>
</span>
</span>
<span class="choice-body">
<span class="choice-title">Start from scratch</span>
<span class="choice-sub">Keep the workspace empty and add your own deals when you’re ready.</span>
</span>
<span class="choice-go" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" /></svg>
</span>
</button>
</div>
<p class="modal-foot">You can switch later from the workspace toolbar.</p>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Sample-data / “try with demo” prompt
An empty workspace is the hardest moment in onboarding — there is nothing to click and nothing to learn from. This pattern meets new users with a Start with sample data? prompt offering two clear choices: Load demo data, which fills a faux Tarn CRM deals table with eight fictional example rows, or Start from scratch, which dismisses the prompt and keeps the board empty. Choosing the demo first shows a short shimmering skeleton, then animates the populated rows in and fires a toast confirming the count.
Once demo data is loaded, a Clear demo data button appears in the toolbar; clearing reverts the board to its empty state and brings the prompt back so the flow can be re-tried. The deal count lives in an aria-live region so it is announced to screen readers, the modal traps focus and closes on Esc, and every control is keyboard-usable with visible focus rings.
A segmented control at the top switches the prompt between a centered modal and an inline banner pinned above the table, and flips the choice cards between a preview thumbnail treatment and a compact text-only layout — so the demo shows each variant live. The whole layout is responsive down to 360px, collapsing the table into stacked label/value cards on narrow screens.
Illustrative UI only — fictional companies, owners, and figures.