02 / 20
All 20 in 20 CSS Empty States
- 01No Search Results State
- 02First-Time Onboarding State
- 03Empty Shopping Cart State
- 04Inbox Zero / All Caught Up State
- 05Empty Data Table / No Rows State
- 06Filtered List No Matches State
- 07Empty Dashboard / No Analytics Data
- 08500 Server Error Page Layout
- 09Empty Wishlist / Favorites State
- 10Empty Notification Center Flyout
- 11Failed to Load / Connection Error State
- 12Offline / No Internet Connection State
- 13Permission Denied / Restricted Access State
- 14404 Page Not Found Layout
- 15Deleted or Archived Items State
- 16Skeleton Loading to Empty State Transition
- 17Accessible Empty State (ARIA Live)
- 18Pure CSS Illustrated Empty State
- 19Dark Mode Compatible Empty State
- 20Inline / Micro Empty State Component
First-Time Onboarding State
First run for a team workspace, where the empty state is the onboarding: three real setup steps, a progress ring in the sidebar that tracks them, and a template rail for people who would rather copy than configure. Ticking a step rewrites the heading and the body copy, so the surface earns its place instead of repeating itself .
Nucleo: 44,000+ SVG icons in five families · All Icons Bundle $149
Nucleo · Affiliate link
The code
<div class="est-02">
<div class="est-02__shell">
<aside class="est-02__side">
<button class="est-02__ws" type="button">
<span class="est-02__wsmark" aria-hidden="true">NL</span>
<span class="est-02__wstext"><span class="est-02__wsname">Northline</span><span class="est-02__wsplan">Team workspace</span></span>
<svg class="est-02__wschev" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m8 10 4 4 4-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<nav class="est-02__nav" aria-label="Workspace">
<a class="est-02__nl est-02__nl--on" href="#est-02" aria-current="page"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><rect x="4" y="4.5" width="7" height="7" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="13" y="4.5" width="7" height="7" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="4" y="13.5" width="7" height="6" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="13" y="13.5" width="7" height="6" rx="2" stroke="currentColor" stroke-width="1.6"/></svg>Projects</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M5 6.5h14M5 12h14M5 17.5h9" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>My tasks</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="9" cy="9" r="3.2" stroke="currentColor" stroke-width="1.6"/><path d="M3.5 19c0-3 2.5-5 5.5-5s5.5 2 5.5 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M16 7.2a3 3 0 0 1 0 5.6M17.5 19c0-2-.6-3.4-1.6-4.4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>People</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="7.5" stroke="currentColor" stroke-width="1.6"/><path d="M12 8.5v4l2.5 1.8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>Activity</a>
</nav>
<div class="est-02__sfoot">
<span class="est-02__ring" id="est-02-ring" aria-hidden="true"><span class="est-02__ringtext" id="est-02-ringtext">0/3</span></span>
<span class="est-02__sfoottext">Workspace setup</span>
</div>
</aside>
<div class="est-02__main">
<header class="est-02__top">
<span class="est-02__crumb">Projects</span>
<div class="est-02__topacts">
<button class="est-02__ghost" type="button">Invite</button>
<span class="est-02__ava" aria-hidden="true">EM</span>
</div>
</header>
<div class="est-02__canvas">
<section class="est-02__setup" id="est-02-setup">
<div class="est-02__setuphead">
<h1 class="est-02__h1" id="est-02-title">Three steps and Northline is yours</h1>
<p class="est-02__sub" id="est-02-subtitle">Nothing here yet. Set the basics and your first board builds itself.</p>
</div>
<ol class="est-02__steps" id="est-02-steps">
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="1" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Name your first project</span><span class="est-02__sdesc">Boards, timeline and docs come with it.</span></span>
</li>
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="2" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Invite two teammates</span><span class="est-02__sdesc">Assignees make the board useful.</span></span>
</li>
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="3" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Connect a repository</span><span class="est-02__sdesc">Commits close tasks automatically.</span></span>
</li>
</ol>
<div class="est-02__cta">
<button class="est-02__pri" type="button" id="est-02-pri">Create your first project</button>
<button class="est-02__link" type="button" id="est-02-reset">Start over</button>
</div>
</section>
<aside class="est-02__templates">
<p class="est-02__tlabel">Start from a template</p>
<ul class="est-02__tlist">
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="sprint" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Product sprint</span><span class="est-02__tpld">Backlog, sprint, review</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="launch" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Go-to-market launch</span><span class="est-02__tpld">Assets, channels, dates</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="hiring" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Hiring pipeline</span><span class="est-02__tpld">Stages and scorecards</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="support" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Support triage</span><span class="est-02__tpld">Severity and rotation</span></span></button></li>
</ul>
</aside>
</div>
</div>
</div>
</div><div class="est-02">
<div class="est-02__shell">
<aside class="est-02__side">
<button class="est-02__ws" type="button">
<span class="est-02__wsmark" aria-hidden="true">NL</span>
<span class="est-02__wstext"><span class="est-02__wsname">Northline</span><span class="est-02__wsplan">Team workspace</span></span>
<svg class="est-02__wschev" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m8 10 4 4 4-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<nav class="est-02__nav" aria-label="Workspace">
<a class="est-02__nl est-02__nl--on" href="#est-02" aria-current="page"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><rect x="4" y="4.5" width="7" height="7" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="13" y="4.5" width="7" height="7" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="4" y="13.5" width="7" height="6" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="13" y="13.5" width="7" height="6" rx="2" stroke="currentColor" stroke-width="1.6"/></svg>Projects</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M5 6.5h14M5 12h14M5 17.5h9" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>My tasks</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="9" cy="9" r="3.2" stroke="currentColor" stroke-width="1.6"/><path d="M3.5 19c0-3 2.5-5 5.5-5s5.5 2 5.5 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M16 7.2a3 3 0 0 1 0 5.6M17.5 19c0-2-.6-3.4-1.6-4.4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>People</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="7.5" stroke="currentColor" stroke-width="1.6"/><path d="M12 8.5v4l2.5 1.8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>Activity</a>
</nav>
<div class="est-02__sfoot">
<span class="est-02__ring" id="est-02-ring" aria-hidden="true"><span class="est-02__ringtext" id="est-02-ringtext">0/3</span></span>
<span class="est-02__sfoottext">Workspace setup</span>
</div>
</aside>
<div class="est-02__main">
<header class="est-02__top">
<span class="est-02__crumb">Projects</span>
<div class="est-02__topacts">
<button class="est-02__ghost" type="button">Invite</button>
<span class="est-02__ava" aria-hidden="true">EM</span>
</div>
</header>
<div class="est-02__canvas">
<section class="est-02__setup" id="est-02-setup">
<div class="est-02__setuphead">
<h1 class="est-02__h1" id="est-02-title">Three steps and Northline is yours</h1>
<p class="est-02__sub" id="est-02-subtitle">Nothing here yet. Set the basics and your first board builds itself.</p>
</div>
<ol class="est-02__steps" id="est-02-steps">
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="1" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Name your first project</span><span class="est-02__sdesc">Boards, timeline and docs come with it.</span></span>
</li>
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="2" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Invite two teammates</span><span class="est-02__sdesc">Assignees make the board useful.</span></span>
</li>
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="3" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Connect a repository</span><span class="est-02__sdesc">Commits close tasks automatically.</span></span>
</li>
</ol>
<div class="est-02__cta">
<button class="est-02__pri" type="button" id="est-02-pri">Create your first project</button>
<button class="est-02__link" type="button" id="est-02-reset">Start over</button>
</div>
</section>
<aside class="est-02__templates">
<p class="est-02__tlabel">Start from a template</p>
<ul class="est-02__tlist">
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="sprint" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Product sprint</span><span class="est-02__tpld">Backlog, sprint, review</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="launch" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Go-to-market launch</span><span class="est-02__tpld">Assets, channels, dates</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="hiring" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Hiring pipeline</span><span class="est-02__tpld">Stages and scorecards</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="support" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Support triage</span><span class="est-02__tpld">Severity and rotation</span></span></button></li>
</ul>
</aside>
</div>
</div>
</div>
</div>@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-02 {
--bg: #f7f7f5;
--surface: #ffffff;
--sunk: #f1f0ec;
--line: #e6e5e0;
--line-2: #d9d8d2;
--ink: #16161a;
--muted: #5f6068;
--faint: #8d8e95;
--accent: #2b3a67;
--ok: #2f6f4f;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-02 *,
.est-02 *::before,
.est-02 *::after {
box-sizing: border-box;
}
.est-02 [hidden] {
display: none !important;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-02 {
--bg: oklch(0.975 0.003 85);
--surface: oklch(1 0 0);
--sunk: oklch(0.955 0.005 85);
--line: oklch(0.912 0.005 85);
--line-2: oklch(0.865 0.006 85);
--ink: oklch(0.18 0.008 275);
--muted: oklch(0.48 0.011 275);
--faint: oklch(0.635 0.011 275);
--accent: oklch(0.36 0.075 265);
--ok: oklch(0.5 0.09 155);
}
}
.est-02__shell {
min-height: 100vh;
min-height: 100svh;
display: grid;
grid-template-columns: min(17rem,26vw) minmax(0,1fr);
}
.est-02__side {
display: grid;
grid-template-rows: auto minmax(0,1fr) auto;
gap: .5rem;
padding: .85rem .7rem 1rem;
border-inline-end: 1px solid var(--line);
background: var(--surface);
}
.est-02__ws {
display: grid;
grid-template-columns: auto minmax(0,1fr) auto;
gap: .6rem;
align-items: center;
text-align: start;
padding: .55rem .6rem;
cursor: pointer;
background: transparent;
border: 1px solid transparent;
border-radius: 12px;
transition: background .16s, border-color .16s;
}
.est-02__ws:hover {
background: var(--sunk);
border-color: var(--line);
}
.est-02__wsmark {
inline-size: 2.1rem;
block-size: 2.1rem;
display: grid;
place-items: center;
border-radius: 9px;
background: var(--accent);
color: #fff;
font-size: .72rem;
font-weight: 600;
letter-spacing: .02em;
}
.est-02__wstext {
min-inline-size: 0;
display: grid;
}
.est-02__wsname {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.est-02__wsplan {
font-size: .72rem;
color: var(--faint);
}
.est-02__wschev {
inline-size: 1rem;
block-size: 1rem;
color: var(--faint);
}
.est-02__nav {
display: grid;
gap: .1rem;
align-content: start;
}
.est-02__nl {
display: flex;
align-items: center;
gap: .6rem;
padding: .5rem .6rem;
border-radius: 9px;
text-decoration: none;
font-size: .875rem;
font-weight: 500;
color: var(--muted);
transition: background .16s, color .16s;
}
.est-02__nl svg {
inline-size: 1.05rem;
block-size: 1.05rem;
flex: none;
opacity: .85;
}
.est-02__nl:hover {
background: var(--sunk);
color: var(--ink);
}
.est-02__nl--on {
background: var(--sunk);
color: var(--ink);
box-shadow: inset 0 0 0 1px var(--line);
}
.est-02__sfoot {
display: flex;
align-items: center;
gap: .6rem;
padding: .7rem .6rem;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--sunk);
}
.est-02__ring {
position: relative;
inline-size: 2.15rem;
block-size: 2.15rem;
flex: none;
display: grid;
place-items: center;
border-radius: 50%;
--p: 0deg;
background: conic-gradient(var(--accent) var(--p), color-mix(in oklab, var(--line-2) 70%, transparent) 0);
transition: --p .5s ease;
}
.est-02__ring::after {
content: '';
position: absolute;
inset: 3px;
border-radius: 50%;
background: var(--sunk);
}
.est-02__ringtext {
position: relative;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .62rem;
color: var(--muted);
}
.est-02__sfoottext {
font-size: .8rem;
color: var(--muted);
}
.est-02__main {
min-inline-size: 0;
display: grid;
grid-template-rows: auto minmax(0,1fr);
}
.est-02__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: .85rem clamp(1rem,3vw,2rem);
border-block-end: 1px solid var(--line);
background: color-mix(in oklab, var(--surface) 80%, transparent);
backdrop-filter: blur(6px);
}
.est-02__crumb {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
}
.est-02__topacts {
display: flex;
align-items: center;
gap: .6rem;
}
.est-02__ghost {
min-block-size: 2.25rem;
padding: .45rem .8rem;
font: inherit;
font-size: .83rem;
font-weight: 500;
cursor: pointer;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s;
}
.est-02__ghost:hover {
background: var(--sunk);
}
.est-02__ava {
inline-size: 1.9rem;
block-size: 1.9rem;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--sunk);
border: 1px solid var(--line);
font-size: .68rem;
font-weight: 600;
color: var(--muted);
}
.est-02__canvas {
min-inline-size: 0;
padding: clamp(1.25rem,4vw,3.25rem) clamp(1rem,3vw,2rem);
display: grid;
gap: clamp(1rem,2.5vw,2rem);
grid-template-columns: repeat(auto-fit,minmax(min(19rem,100%),1fr));
align-content: start;
align-items: start;
background: radial-gradient(120% 90% at 12% 0%, color-mix(in oklab, var(--accent) 5%, transparent), transparent 60%), var(--bg);
}
.est-02__setup {
grid-column: span 2;
inline-size: min(38rem,100%);
display: grid;
gap: 1.35rem;
padding: clamp(1.25rem,3vw,2rem);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 20px;
box-shadow: 0 1px 2px rgba(20,20,25,.04), 0 30px 60px -46px rgba(20,20,25,.4);
animation: est-02-rise .55s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-02-rise {
from {
opacity: 0;
translate: 0 10px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-02__setuphead {
display: grid;
gap: .45rem;
}
.est-02__h1 {
margin: 0;
font-size: clamp(1.4rem,3.2vw,1.95rem);
font-weight: 600;
letter-spacing: -.032em;
line-height: 1.12;
text-wrap: balance;
}
.est-02__sub {
margin: 0;
max-inline-size: 44ch;
font-size: .95rem;
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.est-02__steps {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .5rem;
}
.est-02__step {
display: grid;
grid-template-columns: auto minmax(0,1fr);
gap: .85rem;
align-items: center;
padding: .8rem .9rem;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
transition: border-color .18s, background .18s;
}
.est-02__step:hover {
border-color: var(--line-2);
background: var(--sunk);
}
.est-02__tick {
inline-size: 1.6rem;
block-size: 1.6rem;
display: grid;
place-items: center;
cursor: pointer;
flex: none;
color: transparent;
background: var(--surface);
border: 1.5px solid var(--line-2);
border-radius: 50%;
transition: background .2s, border-color .2s, color .2s, scale .2s;
}
.est-02__tick svg {
inline-size: .95rem;
block-size: .95rem;
}
.est-02__tick:hover {
border-color: var(--accent);
}
.est-02__tick[aria-pressed="true"] {
color: #fff;
background: var(--accent);
border-color: var(--accent);
scale: 1.04;
}
.est-02__stext {
min-inline-size: 0;
display: grid;
gap: .15rem;
}
.est-02__sname {
font-size: .92rem;
font-weight: 500;
letter-spacing: -.015em;
}
.est-02__step[data-done] .est-02__sname {
color: var(--muted);
text-decoration: line-through;
text-decoration-color: var(--line-2);
}
.est-02__sdesc {
font-size: .8rem;
color: var(--faint);
}
.est-02__cta {
display: flex;
flex-wrap: wrap;
gap: .75rem;
align-items: center;
}
.est-02__pri {
min-block-size: 2.85rem;
padding: .7rem 1.15rem;
font: inherit;
font-size: .9rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 12px;
transition: translate .16s, box-shadow .18s, background .16s;
box-shadow: 0 8px 18px -12px color-mix(in oklab, var(--accent) 75%, transparent);
}
.est-02__pri:hover {
translate: 0 -1px;
box-shadow: 0 14px 26px -14px color-mix(in oklab, var(--accent) 70%, transparent);
}
.est-02__link {
padding: .4rem .1rem;
font: inherit;
font-size: .85rem;
cursor: pointer;
color: var(--muted);
background: none;
border: 0;
text-decoration: underline;
text-decoration-color: var(--line-2);
text-underline-offset: 3px;
}
.est-02__link:hover {
color: var(--ink);
}
.est-02__tick:focus-visible,
.est-02__pri:focus-visible,
.est-02__link:focus-visible,
.est-02__tpl:focus-visible,
.est-02__ghost:focus-visible,
.est-02__ws:focus-visible,
.est-02__nl:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.est-02__templates {
inline-size: min(22rem,100%);
display: grid;
gap: .6rem;
align-content: start;
}
.est-02__tlabel {
margin: 0;
font-size: .8rem;
font-weight: 500;
color: var(--faint);
}
.est-02__tlist {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .4rem;
}
.est-02__tpl {
inline-size: 100%;
display: grid;
grid-template-columns: auto minmax(0,1fr);
gap: .7rem;
align-items: center;
text-align: start;
padding: .7rem .8rem;
cursor: pointer;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 13px;
transition: translate .16s, border-color .16s, box-shadow .18s;
}
.est-02__tpl:hover {
translate: 0 -1px;
border-color: var(--line-2);
box-shadow: 0 12px 24px -20px rgba(20,20,25,.5);
}
.est-02__tplg {
inline-size: 2rem;
block-size: 2rem;
border-radius: 8px;
background: var(--sunk);
box-shadow: inset 0 0 0 1px var(--line);
position: relative;
overflow: hidden;
}
.est-02__tplg::after {
content: '';
position: absolute;
inset: .35rem;
border-radius: 3px;
background: repeating-linear-gradient(90deg, color-mix(in oklab, var(--accent) 45%, transparent) 0 2px, transparent 2px 5px);
opacity: .7;
}
.est-02__tplg[data-g="launch"]::after {
background: conic-gradient(from 210deg, color-mix(in oklab, var(--accent) 50%, transparent), transparent 60%);
}
.est-02__tplg[data-g="hiring"]::after {
background: linear-gradient(180deg, color-mix(in oklab, var(--accent) 45%, transparent) 0 30%, transparent 30% 45%, color-mix(in oklab, var(--accent) 30%, transparent) 45% 70%, transparent 70%);
}
.est-02__tplg[data-g="support"]::after {
background: radial-gradient(circle at 50% 50%, color-mix(in oklab, var(--accent) 55%, transparent) 30%, transparent 32%);
}
.est-02__tpltext {
min-inline-size: 0;
display: grid;
gap: .1rem;
}
.est-02__tpln {
font-size: .87rem;
font-weight: 500;
letter-spacing: -.012em;
}
.est-02__tpld {
font-size: .76rem;
color: var(--faint);
}
.est-02__setup[data-ready] {
border-color: color-mix(in oklab, var(--ok) 35%, var(--line));
}
@media (max-width: 900px) {
.est-02__shell {
grid-template-columns: 1fr;
}
.est-02__side {
grid-template-rows: auto;
grid-template-columns: auto minmax(0,1fr) auto;
align-items: center;
border-inline-end: 0;
border-block-end: 1px solid var(--line);
}
.est-02__nav {
display: none;
}
.est-02__setup {
grid-column: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.est-02 *,
.est-02 *::before,
.est-02 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-02__setup,
.est-02__step,
.est-02__tpl,
.est-02__pri {
border-color: CanvasText;
}
}@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-02 {
--bg: #f7f7f5;
--surface: #ffffff;
--sunk: #f1f0ec;
--line: #e6e5e0;
--line-2: #d9d8d2;
--ink: #16161a;
--muted: #5f6068;
--faint: #8d8e95;
--accent: #2b3a67;
--ok: #2f6f4f;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-02 *,
.est-02 *::before,
.est-02 *::after {
box-sizing: border-box;
}
.est-02 [hidden] {
display: none !important;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-02 {
--bg: oklch(0.975 0.003 85);
--surface: oklch(1 0 0);
--sunk: oklch(0.955 0.005 85);
--line: oklch(0.912 0.005 85);
--line-2: oklch(0.865 0.006 85);
--ink: oklch(0.18 0.008 275);
--muted: oklch(0.48 0.011 275);
--faint: oklch(0.635 0.011 275);
--accent: oklch(0.36 0.075 265);
--ok: oklch(0.5 0.09 155);
}
}
.est-02__shell {
min-height: 100vh;
min-height: 100svh;
display: grid;
grid-template-columns: min(17rem,26vw) minmax(0,1fr);
}
.est-02__side {
display: grid;
grid-template-rows: auto minmax(0,1fr) auto;
gap: .5rem;
padding: .85rem .7rem 1rem;
border-inline-end: 1px solid var(--line);
background: var(--surface);
}
.est-02__ws {
display: grid;
grid-template-columns: auto minmax(0,1fr) auto;
gap: .6rem;
align-items: center;
text-align: start;
padding: .55rem .6rem;
cursor: pointer;
background: transparent;
border: 1px solid transparent;
border-radius: 12px;
transition: background .16s, border-color .16s;
}
.est-02__ws:hover {
background: var(--sunk);
border-color: var(--line);
}
.est-02__wsmark {
inline-size: 2.1rem;
block-size: 2.1rem;
display: grid;
place-items: center;
border-radius: 9px;
background: var(--accent);
color: #fff;
font-size: .72rem;
font-weight: 600;
letter-spacing: .02em;
}
.est-02__wstext {
min-inline-size: 0;
display: grid;
}
.est-02__wsname {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.est-02__wsplan {
font-size: .72rem;
color: var(--faint);
}
.est-02__wschev {
inline-size: 1rem;
block-size: 1rem;
color: var(--faint);
}
.est-02__nav {
display: grid;
gap: .1rem;
align-content: start;
}
.est-02__nl {
display: flex;
align-items: center;
gap: .6rem;
padding: .5rem .6rem;
border-radius: 9px;
text-decoration: none;
font-size: .875rem;
font-weight: 500;
color: var(--muted);
transition: background .16s, color .16s;
}
.est-02__nl svg {
inline-size: 1.05rem;
block-size: 1.05rem;
flex: none;
opacity: .85;
}
.est-02__nl:hover {
background: var(--sunk);
color: var(--ink);
}
.est-02__nl--on {
background: var(--sunk);
color: var(--ink);
box-shadow: inset 0 0 0 1px var(--line);
}
.est-02__sfoot {
display: flex;
align-items: center;
gap: .6rem;
padding: .7rem .6rem;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--sunk);
}
.est-02__ring {
position: relative;
inline-size: 2.15rem;
block-size: 2.15rem;
flex: none;
display: grid;
place-items: center;
border-radius: 50%;
--p: 0deg;
background: conic-gradient(var(--accent) var(--p), color-mix(in oklab, var(--line-2) 70%, transparent) 0);
transition: --p .5s ease;
}
.est-02__ring::after {
content: '';
position: absolute;
inset: 3px;
border-radius: 50%;
background: var(--sunk);
}
.est-02__ringtext {
position: relative;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .62rem;
color: var(--muted);
}
.est-02__sfoottext {
font-size: .8rem;
color: var(--muted);
}
.est-02__main {
min-inline-size: 0;
display: grid;
grid-template-rows: auto minmax(0,1fr);
}
.est-02__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: .85rem clamp(1rem,3vw,2rem);
border-block-end: 1px solid var(--line);
background: color-mix(in oklab, var(--surface) 80%, transparent);
backdrop-filter: blur(6px);
}
.est-02__crumb {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
}
.est-02__topacts {
display: flex;
align-items: center;
gap: .6rem;
}
.est-02__ghost {
min-block-size: 2.25rem;
padding: .45rem .8rem;
font: inherit;
font-size: .83rem;
font-weight: 500;
cursor: pointer;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s;
}
.est-02__ghost:hover {
background: var(--sunk);
}
.est-02__ava {
inline-size: 1.9rem;
block-size: 1.9rem;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--sunk);
border: 1px solid var(--line);
font-size: .68rem;
font-weight: 600;
color: var(--muted);
}
.est-02__canvas {
min-inline-size: 0;
padding: clamp(1.25rem,4vw,3.25rem) clamp(1rem,3vw,2rem);
display: grid;
gap: clamp(1rem,2.5vw,2rem);
grid-template-columns: repeat(auto-fit,minmax(min(19rem,100%),1fr));
align-content: start;
align-items: start;
background: radial-gradient(120% 90% at 12% 0%, color-mix(in oklab, var(--accent) 5%, transparent), transparent 60%), var(--bg);
}
.est-02__setup {
grid-column: span 2;
inline-size: min(38rem,100%);
display: grid;
gap: 1.35rem;
padding: clamp(1.25rem,3vw,2rem);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 20px;
box-shadow: 0 1px 2px rgba(20,20,25,.04), 0 30px 60px -46px rgba(20,20,25,.4);
animation: est-02-rise .55s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-02-rise {
from {
opacity: 0;
translate: 0 10px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-02__setuphead {
display: grid;
gap: .45rem;
}
.est-02__h1 {
margin: 0;
font-size: clamp(1.4rem,3.2vw,1.95rem);
font-weight: 600;
letter-spacing: -.032em;
line-height: 1.12;
text-wrap: balance;
}
.est-02__sub {
margin: 0;
max-inline-size: 44ch;
font-size: .95rem;
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.est-02__steps {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .5rem;
}
.est-02__step {
display: grid;
grid-template-columns: auto minmax(0,1fr);
gap: .85rem;
align-items: center;
padding: .8rem .9rem;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
transition: border-color .18s, background .18s;
}
.est-02__step:hover {
border-color: var(--line-2);
background: var(--sunk);
}
.est-02__tick {
inline-size: 1.6rem;
block-size: 1.6rem;
display: grid;
place-items: center;
cursor: pointer;
flex: none;
color: transparent;
background: var(--surface);
border: 1.5px solid var(--line-2);
border-radius: 50%;
transition: background .2s, border-color .2s, color .2s, scale .2s;
}
.est-02__tick svg {
inline-size: .95rem;
block-size: .95rem;
}
.est-02__tick:hover {
border-color: var(--accent);
}
.est-02__tick[aria-pressed="true"] {
color: #fff;
background: var(--accent);
border-color: var(--accent);
scale: 1.04;
}
.est-02__stext {
min-inline-size: 0;
display: grid;
gap: .15rem;
}
.est-02__sname {
font-size: .92rem;
font-weight: 500;
letter-spacing: -.015em;
}
.est-02__step[data-done] .est-02__sname {
color: var(--muted);
text-decoration: line-through;
text-decoration-color: var(--line-2);
}
.est-02__sdesc {
font-size: .8rem;
color: var(--faint);
}
.est-02__cta {
display: flex;
flex-wrap: wrap;
gap: .75rem;
align-items: center;
}
.est-02__pri {
min-block-size: 2.85rem;
padding: .7rem 1.15rem;
font: inherit;
font-size: .9rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 12px;
transition: translate .16s, box-shadow .18s, background .16s;
box-shadow: 0 8px 18px -12px color-mix(in oklab, var(--accent) 75%, transparent);
}
.est-02__pri:hover {
translate: 0 -1px;
box-shadow: 0 14px 26px -14px color-mix(in oklab, var(--accent) 70%, transparent);
}
.est-02__link {
padding: .4rem .1rem;
font: inherit;
font-size: .85rem;
cursor: pointer;
color: var(--muted);
background: none;
border: 0;
text-decoration: underline;
text-decoration-color: var(--line-2);
text-underline-offset: 3px;
}
.est-02__link:hover {
color: var(--ink);
}
.est-02__tick:focus-visible,
.est-02__pri:focus-visible,
.est-02__link:focus-visible,
.est-02__tpl:focus-visible,
.est-02__ghost:focus-visible,
.est-02__ws:focus-visible,
.est-02__nl:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.est-02__templates {
inline-size: min(22rem,100%);
display: grid;
gap: .6rem;
align-content: start;
}
.est-02__tlabel {
margin: 0;
font-size: .8rem;
font-weight: 500;
color: var(--faint);
}
.est-02__tlist {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .4rem;
}
.est-02__tpl {
inline-size: 100%;
display: grid;
grid-template-columns: auto minmax(0,1fr);
gap: .7rem;
align-items: center;
text-align: start;
padding: .7rem .8rem;
cursor: pointer;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 13px;
transition: translate .16s, border-color .16s, box-shadow .18s;
}
.est-02__tpl:hover {
translate: 0 -1px;
border-color: var(--line-2);
box-shadow: 0 12px 24px -20px rgba(20,20,25,.5);
}
.est-02__tplg {
inline-size: 2rem;
block-size: 2rem;
border-radius: 8px;
background: var(--sunk);
box-shadow: inset 0 0 0 1px var(--line);
position: relative;
overflow: hidden;
}
.est-02__tplg::after {
content: '';
position: absolute;
inset: .35rem;
border-radius: 3px;
background: repeating-linear-gradient(90deg, color-mix(in oklab, var(--accent) 45%, transparent) 0 2px, transparent 2px 5px);
opacity: .7;
}
.est-02__tplg[data-g="launch"]::after {
background: conic-gradient(from 210deg, color-mix(in oklab, var(--accent) 50%, transparent), transparent 60%);
}
.est-02__tplg[data-g="hiring"]::after {
background: linear-gradient(180deg, color-mix(in oklab, var(--accent) 45%, transparent) 0 30%, transparent 30% 45%, color-mix(in oklab, var(--accent) 30%, transparent) 45% 70%, transparent 70%);
}
.est-02__tplg[data-g="support"]::after {
background: radial-gradient(circle at 50% 50%, color-mix(in oklab, var(--accent) 55%, transparent) 30%, transparent 32%);
}
.est-02__tpltext {
min-inline-size: 0;
display: grid;
gap: .1rem;
}
.est-02__tpln {
font-size: .87rem;
font-weight: 500;
letter-spacing: -.012em;
}
.est-02__tpld {
font-size: .76rem;
color: var(--faint);
}
.est-02__setup[data-ready] {
border-color: color-mix(in oklab, var(--ok) 35%, var(--line));
}
@media (max-width: 900px) {
.est-02__shell {
grid-template-columns: 1fr;
}
.est-02__side {
grid-template-rows: auto;
grid-template-columns: auto minmax(0,1fr) auto;
align-items: center;
border-inline-end: 0;
border-block-end: 1px solid var(--line);
}
.est-02__nav {
display: none;
}
.est-02__setup {
grid-column: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.est-02 *,
.est-02 *::before,
.est-02 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-02__setup,
.est-02__step,
.est-02__tpl,
.est-02__pri {
border-color: CanvasText;
}
}(function(){
var root = document.querySelector('.est-02'); if(!root) return;
var ring = root.querySelector('#est-02-ring'), ringtext = root.querySelector('#est-02-ringtext'),
title = root.querySelector('#est-02-title'), sub = root.querySelector('#est-02-subtitle'),
setup = root.querySelector('#est-02-setup'), pri = root.querySelector('#est-02-pri'),
reset = root.querySelector('#est-02-reset'), ticks = root.querySelectorAll('.est-02__tick');
var copy = [
['Three steps and Northline is yours','Nothing here yet. Set the basics and your first board builds itself.'],
['Good start','Two more and the board fills with your own work.'],
['Almost there','One step left before Northline runs itself.'],
['Workspace ready','Create the project and your team lands straight in it.']
];
function paint(){
var done = 0;
ticks.forEach(function(t){
var on = t.getAttribute('aria-pressed') === 'true';
if(on) done++;
t.closest('.est-02__step').toggleAttribute('data-done', on);
});
ring.style.setProperty('--p', (done / 3 * 360) + 'deg');
ringtext.textContent = done + '/3';
title.textContent = copy[done][0];
sub.textContent = copy[done][1];
setup.toggleAttribute('data-ready', done === 3);
pri.textContent = done === 3 ? 'Open your project' : 'Create your first project';
}
ticks.forEach(function(t){
t.addEventListener('click', function(){
t.setAttribute('aria-pressed', t.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
paint();
});
});
reset.addEventListener('click', function(){
ticks.forEach(function(t){ t.setAttribute('aria-pressed','false'); });
paint();
});
paint();
})();(function(){
var root = document.querySelector('.est-02'); if(!root) return;
var ring = root.querySelector('#est-02-ring'), ringtext = root.querySelector('#est-02-ringtext'),
title = root.querySelector('#est-02-title'), sub = root.querySelector('#est-02-subtitle'),
setup = root.querySelector('#est-02-setup'), pri = root.querySelector('#est-02-pri'),
reset = root.querySelector('#est-02-reset'), ticks = root.querySelectorAll('.est-02__tick');
var copy = [
['Three steps and Northline is yours','Nothing here yet. Set the basics and your first board builds itself.'],
['Good start','Two more and the board fills with your own work.'],
['Almost there','One step left before Northline runs itself.'],
['Workspace ready','Create the project and your team lands straight in it.']
];
function paint(){
var done = 0;
ticks.forEach(function(t){
var on = t.getAttribute('aria-pressed') === 'true';
if(on) done++;
t.closest('.est-02__step').toggleAttribute('data-done', on);
});
ring.style.setProperty('--p', (done / 3 * 360) + 'deg');
ringtext.textContent = done + '/3';
title.textContent = copy[done][0];
sub.textContent = copy[done][1];
setup.toggleAttribute('data-ready', done === 3);
pri.textContent = done === 3 ? 'Open your project' : 'Create your first project';
}
ticks.forEach(function(t){
t.addEventListener('click', function(){
t.setAttribute('aria-pressed', t.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
paint();
});
});
reset.addEventListener('click', function(){
ticks.forEach(function(t){ t.setAttribute('aria-pressed','false'); });
paint();
});
paint();
})();Here's a working CSS Empty State from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: First-Time Onboarding State
Source: https://codefronts.com/snippets/css-empty-states/first-time-onboarding-state/
First run for a team workspace, where the empty state is the onboarding: three real setup steps, a progress ring in the sidebar that tracks them, and a template rail for people who would rather copy than configure. Ticking a step rewrites the heading and the body copy, so the surface earns its place instead of repeating itself .
## HTML
```html
<div class="est-02">
<div class="est-02__shell">
<aside class="est-02__side">
<button class="est-02__ws" type="button">
<span class="est-02__wsmark" aria-hidden="true">NL</span>
<span class="est-02__wstext"><span class="est-02__wsname">Northline</span><span class="est-02__wsplan">Team workspace</span></span>
<svg class="est-02__wschev" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m8 10 4 4 4-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<nav class="est-02__nav" aria-label="Workspace">
<a class="est-02__nl est-02__nl--on" href="#est-02" aria-current="page"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><rect x="4" y="4.5" width="7" height="7" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="13" y="4.5" width="7" height="7" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="4" y="13.5" width="7" height="6" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="13" y="13.5" width="7" height="6" rx="2" stroke="currentColor" stroke-width="1.6"/></svg>Projects</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M5 6.5h14M5 12h14M5 17.5h9" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>My tasks</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="9" cy="9" r="3.2" stroke="currentColor" stroke-width="1.6"/><path d="M3.5 19c0-3 2.5-5 5.5-5s5.5 2 5.5 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M16 7.2a3 3 0 0 1 0 5.6M17.5 19c0-2-.6-3.4-1.6-4.4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>People</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="7.5" stroke="currentColor" stroke-width="1.6"/><path d="M12 8.5v4l2.5 1.8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>Activity</a>
</nav>
<div class="est-02__sfoot">
<span class="est-02__ring" id="est-02-ring" aria-hidden="true"><span class="est-02__ringtext" id="est-02-ringtext">0/3</span></span>
<span class="est-02__sfoottext">Workspace setup</span>
</div>
</aside>
<div class="est-02__main">
<header class="est-02__top">
<span class="est-02__crumb">Projects</span>
<div class="est-02__topacts">
<button class="est-02__ghost" type="button">Invite</button>
<span class="est-02__ava" aria-hidden="true">EM</span>
</div>
</header>
<div class="est-02__canvas">
<section class="est-02__setup" id="est-02-setup">
<div class="est-02__setuphead">
<h1 class="est-02__h1" id="est-02-title">Three steps and Northline is yours</h1>
<p class="est-02__sub" id="est-02-subtitle">Nothing here yet. Set the basics and your first board builds itself.</p>
</div>
<ol class="est-02__steps" id="est-02-steps">
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="1" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Name your first project</span><span class="est-02__sdesc">Boards, timeline and docs come with it.</span></span>
</li>
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="2" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Invite two teammates</span><span class="est-02__sdesc">Assignees make the board useful.</span></span>
</li>
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="3" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Connect a repository</span><span class="est-02__sdesc">Commits close tasks automatically.</span></span>
</li>
</ol>
<div class="est-02__cta">
<button class="est-02__pri" type="button" id="est-02-pri">Create your first project</button>
<button class="est-02__link" type="button" id="est-02-reset">Start over</button>
</div>
</section>
<aside class="est-02__templates">
<p class="est-02__tlabel">Start from a template</p>
<ul class="est-02__tlist">
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="sprint" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Product sprint</span><span class="est-02__tpld">Backlog, sprint, review</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="launch" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Go-to-market launch</span><span class="est-02__tpld">Assets, channels, dates</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="hiring" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Hiring pipeline</span><span class="est-02__tpld">Stages and scorecards</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="support" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Support triage</span><span class="est-02__tpld">Severity and rotation</span></span></button></li>
</ul>
</aside>
</div>
</div>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-02 {
--bg: #f7f7f5;
--surface: #ffffff;
--sunk: #f1f0ec;
--line: #e6e5e0;
--line-2: #d9d8d2;
--ink: #16161a;
--muted: #5f6068;
--faint: #8d8e95;
--accent: #2b3a67;
--ok: #2f6f4f;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-02 *,
.est-02 *::before,
.est-02 *::after {
box-sizing: border-box;
}
.est-02 [hidden] {
display: none !important;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-02 {
--bg: oklch(0.975 0.003 85);
--surface: oklch(1 0 0);
--sunk: oklch(0.955 0.005 85);
--line: oklch(0.912 0.005 85);
--line-2: oklch(0.865 0.006 85);
--ink: oklch(0.18 0.008 275);
--muted: oklch(0.48 0.011 275);
--faint: oklch(0.635 0.011 275);
--accent: oklch(0.36 0.075 265);
--ok: oklch(0.5 0.09 155);
}
}
.est-02__shell {
min-height: 100vh;
min-height: 100svh;
display: grid;
grid-template-columns: min(17rem,26vw) minmax(0,1fr);
}
.est-02__side {
display: grid;
grid-template-rows: auto minmax(0,1fr) auto;
gap: .5rem;
padding: .85rem .7rem 1rem;
border-inline-end: 1px solid var(--line);
background: var(--surface);
}
.est-02__ws {
display: grid;
grid-template-columns: auto minmax(0,1fr) auto;
gap: .6rem;
align-items: center;
text-align: start;
padding: .55rem .6rem;
cursor: pointer;
background: transparent;
border: 1px solid transparent;
border-radius: 12px;
transition: background .16s, border-color .16s;
}
.est-02__ws:hover {
background: var(--sunk);
border-color: var(--line);
}
.est-02__wsmark {
inline-size: 2.1rem;
block-size: 2.1rem;
display: grid;
place-items: center;
border-radius: 9px;
background: var(--accent);
color: #fff;
font-size: .72rem;
font-weight: 600;
letter-spacing: .02em;
}
.est-02__wstext {
min-inline-size: 0;
display: grid;
}
.est-02__wsname {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.est-02__wsplan {
font-size: .72rem;
color: var(--faint);
}
.est-02__wschev {
inline-size: 1rem;
block-size: 1rem;
color: var(--faint);
}
.est-02__nav {
display: grid;
gap: .1rem;
align-content: start;
}
.est-02__nl {
display: flex;
align-items: center;
gap: .6rem;
padding: .5rem .6rem;
border-radius: 9px;
text-decoration: none;
font-size: .875rem;
font-weight: 500;
color: var(--muted);
transition: background .16s, color .16s;
}
.est-02__nl svg {
inline-size: 1.05rem;
block-size: 1.05rem;
flex: none;
opacity: .85;
}
.est-02__nl:hover {
background: var(--sunk);
color: var(--ink);
}
.est-02__nl--on {
background: var(--sunk);
color: var(--ink);
box-shadow: inset 0 0 0 1px var(--line);
}
.est-02__sfoot {
display: flex;
align-items: center;
gap: .6rem;
padding: .7rem .6rem;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--sunk);
}
.est-02__ring {
position: relative;
inline-size: 2.15rem;
block-size: 2.15rem;
flex: none;
display: grid;
place-items: center;
border-radius: 50%;
--p: 0deg;
background: conic-gradient(var(--accent) var(--p), color-mix(in oklab, var(--line-2) 70%, transparent) 0);
transition: --p .5s ease;
}
.est-02__ring::after {
content: '';
position: absolute;
inset: 3px;
border-radius: 50%;
background: var(--sunk);
}
.est-02__ringtext {
position: relative;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .62rem;
color: var(--muted);
}
.est-02__sfoottext {
font-size: .8rem;
color: var(--muted);
}
.est-02__main {
min-inline-size: 0;
display: grid;
grid-template-rows: auto minmax(0,1fr);
}
.est-02__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: .85rem clamp(1rem,3vw,2rem);
border-block-end: 1px solid var(--line);
background: color-mix(in oklab, var(--surface) 80%, transparent);
backdrop-filter: blur(6px);
}
.est-02__crumb {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
}
.est-02__topacts {
display: flex;
align-items: center;
gap: .6rem;
}
.est-02__ghost {
min-block-size: 2.25rem;
padding: .45rem .8rem;
font: inherit;
font-size: .83rem;
font-weight: 500;
cursor: pointer;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s;
}
.est-02__ghost:hover {
background: var(--sunk);
}
.est-02__ava {
inline-size: 1.9rem;
block-size: 1.9rem;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--sunk);
border: 1px solid var(--line);
font-size: .68rem;
font-weight: 600;
color: var(--muted);
}
.est-02__canvas {
min-inline-size: 0;
padding: clamp(1.25rem,4vw,3.25rem) clamp(1rem,3vw,2rem);
display: grid;
gap: clamp(1rem,2.5vw,2rem);
grid-template-columns: repeat(auto-fit,minmax(min(19rem,100%),1fr));
align-content: start;
align-items: start;
background: radial-gradient(120% 90% at 12% 0%, color-mix(in oklab, var(--accent) 5%, transparent), transparent 60%), var(--bg);
}
.est-02__setup {
grid-column: span 2;
inline-size: min(38rem,100%);
display: grid;
gap: 1.35rem;
padding: clamp(1.25rem,3vw,2rem);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 20px;
box-shadow: 0 1px 2px rgba(20,20,25,.04), 0 30px 60px -46px rgba(20,20,25,.4);
animation: est-02-rise .55s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-02-rise {
from {
opacity: 0;
translate: 0 10px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-02__setuphead {
display: grid;
gap: .45rem;
}
.est-02__h1 {
margin: 0;
font-size: clamp(1.4rem,3.2vw,1.95rem);
font-weight: 600;
letter-spacing: -.032em;
line-height: 1.12;
text-wrap: balance;
}
.est-02__sub {
margin: 0;
max-inline-size: 44ch;
font-size: .95rem;
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.est-02__steps {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .5rem;
}
.est-02__step {
display: grid;
grid-template-columns: auto minmax(0,1fr);
gap: .85rem;
align-items: center;
padding: .8rem .9rem;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
transition: border-color .18s, background .18s;
}
.est-02__step:hover {
border-color: var(--line-2);
background: var(--sunk);
}
.est-02__tick {
inline-size: 1.6rem;
block-size: 1.6rem;
display: grid;
place-items: center;
cursor: pointer;
flex: none;
color: transparent;
background: var(--surface);
border: 1.5px solid var(--line-2);
border-radius: 50%;
transition: background .2s, border-color .2s, color .2s, scale .2s;
}
.est-02__tick svg {
inline-size: .95rem;
block-size: .95rem;
}
.est-02__tick:hover {
border-color: var(--accent);
}
.est-02__tick[aria-pressed="true"] {
color: #fff;
background: var(--accent);
border-color: var(--accent);
scale: 1.04;
}
.est-02__stext {
min-inline-size: 0;
display: grid;
gap: .15rem;
}
.est-02__sname {
font-size: .92rem;
font-weight: 500;
letter-spacing: -.015em;
}
.est-02__step[data-done] .est-02__sname {
color: var(--muted);
text-decoration: line-through;
text-decoration-color: var(--line-2);
}
.est-02__sdesc {
font-size: .8rem;
color: var(--faint);
}
.est-02__cta {
display: flex;
flex-wrap: wrap;
gap: .75rem;
align-items: center;
}
.est-02__pri {
min-block-size: 2.85rem;
padding: .7rem 1.15rem;
font: inherit;
font-size: .9rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 12px;
transition: translate .16s, box-shadow .18s, background .16s;
box-shadow: 0 8px 18px -12px color-mix(in oklab, var(--accent) 75%, transparent);
}
.est-02__pri:hover {
translate: 0 -1px;
box-shadow: 0 14px 26px -14px color-mix(in oklab, var(--accent) 70%, transparent);
}
.est-02__link {
padding: .4rem .1rem;
font: inherit;
font-size: .85rem;
cursor: pointer;
color: var(--muted);
background: none;
border: 0;
text-decoration: underline;
text-decoration-color: var(--line-2);
text-underline-offset: 3px;
}
.est-02__link:hover {
color: var(--ink);
}
.est-02__tick:focus-visible,
.est-02__pri:focus-visible,
.est-02__link:focus-visible,
.est-02__tpl:focus-visible,
.est-02__ghost:focus-visible,
.est-02__ws:focus-visible,
.est-02__nl:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.est-02__templates {
inline-size: min(22rem,100%);
display: grid;
gap: .6rem;
align-content: start;
}
.est-02__tlabel {
margin: 0;
font-size: .8rem;
font-weight: 500;
color: var(--faint);
}
.est-02__tlist {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .4rem;
}
.est-02__tpl {
inline-size: 100%;
display: grid;
grid-template-columns: auto minmax(0,1fr);
gap: .7rem;
align-items: center;
text-align: start;
padding: .7rem .8rem;
cursor: pointer;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 13px;
transition: translate .16s, border-color .16s, box-shadow .18s;
}
.est-02__tpl:hover {
translate: 0 -1px;
border-color: var(--line-2);
box-shadow: 0 12px 24px -20px rgba(20,20,25,.5);
}
.est-02__tplg {
inline-size: 2rem;
block-size: 2rem;
border-radius: 8px;
background: var(--sunk);
box-shadow: inset 0 0 0 1px var(--line);
position: relative;
overflow: hidden;
}
.est-02__tplg::after {
content: '';
position: absolute;
inset: .35rem;
border-radius: 3px;
background: repeating-linear-gradient(90deg, color-mix(in oklab, var(--accent) 45%, transparent) 0 2px, transparent 2px 5px);
opacity: .7;
}
.est-02__tplg[data-g="launch"]::after {
background: conic-gradient(from 210deg, color-mix(in oklab, var(--accent) 50%, transparent), transparent 60%);
}
.est-02__tplg[data-g="hiring"]::after {
background: linear-gradient(180deg, color-mix(in oklab, var(--accent) 45%, transparent) 0 30%, transparent 30% 45%, color-mix(in oklab, var(--accent) 30%, transparent) 45% 70%, transparent 70%);
}
.est-02__tplg[data-g="support"]::after {
background: radial-gradient(circle at 50% 50%, color-mix(in oklab, var(--accent) 55%, transparent) 30%, transparent 32%);
}
.est-02__tpltext {
min-inline-size: 0;
display: grid;
gap: .1rem;
}
.est-02__tpln {
font-size: .87rem;
font-weight: 500;
letter-spacing: -.012em;
}
.est-02__tpld {
font-size: .76rem;
color: var(--faint);
}
.est-02__setup[data-ready] {
border-color: color-mix(in oklab, var(--ok) 35%, var(--line));
}
@media (max-width: 900px) {
.est-02__shell {
grid-template-columns: 1fr;
}
.est-02__side {
grid-template-rows: auto;
grid-template-columns: auto minmax(0,1fr) auto;
align-items: center;
border-inline-end: 0;
border-block-end: 1px solid var(--line);
}
.est-02__nav {
display: none;
}
.est-02__setup {
grid-column: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.est-02 *,
.est-02 *::before,
.est-02 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-02__setup,
.est-02__step,
.est-02__tpl,
.est-02__pri {
border-color: CanvasText;
}
}
```
## JavaScript
```js
(function(){
var root = document.querySelector('.est-02'); if(!root) return;
var ring = root.querySelector('#est-02-ring'), ringtext = root.querySelector('#est-02-ringtext'),
title = root.querySelector('#est-02-title'), sub = root.querySelector('#est-02-subtitle'),
setup = root.querySelector('#est-02-setup'), pri = root.querySelector('#est-02-pri'),
reset = root.querySelector('#est-02-reset'), ticks = root.querySelectorAll('.est-02__tick');
var copy = [
['Three steps and Northline is yours','Nothing here yet. Set the basics and your first board builds itself.'],
['Good start','Two more and the board fills with your own work.'],
['Almost there','One step left before Northline runs itself.'],
['Workspace ready','Create the project and your team lands straight in it.']
];
function paint(){
var done = 0;
ticks.forEach(function(t){
var on = t.getAttribute('aria-pressed') === 'true';
if(on) done++;
t.closest('.est-02__step').toggleAttribute('data-done', on);
});
ring.style.setProperty('--p', (done / 3 * 360) + 'deg');
ringtext.textContent = done + '/3';
title.textContent = copy[done][0];
sub.textContent = copy[done][1];
setup.toggleAttribute('data-ready', done === 3);
pri.textContent = done === 3 ? 'Open your project' : 'Create your first project';
}
ticks.forEach(function(t){
t.addEventListener('click', function(){
t.setAttribute('aria-pressed', t.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
paint();
});
});
reset.addEventListener('click', function(){
ticks.forEach(function(t){ t.setAttribute('aria-pressed','false'); });
paint();
});
paint();
})();
```Here's a working CSS Empty State from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: First-Time Onboarding State
Source: https://codefronts.com/snippets/css-empty-states/first-time-onboarding-state/
First run for a team workspace, where the empty state is the onboarding: three real setup steps, a progress ring in the sidebar that tracks them, and a template rail for people who would rather copy than configure. Ticking a step rewrites the heading and the body copy, so the surface earns its place instead of repeating itself .
## HTML
```html
<div class="est-02">
<div class="est-02__shell">
<aside class="est-02__side">
<button class="est-02__ws" type="button">
<span class="est-02__wsmark" aria-hidden="true">NL</span>
<span class="est-02__wstext"><span class="est-02__wsname">Northline</span><span class="est-02__wsplan">Team workspace</span></span>
<svg class="est-02__wschev" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m8 10 4 4 4-4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<nav class="est-02__nav" aria-label="Workspace">
<a class="est-02__nl est-02__nl--on" href="#est-02" aria-current="page"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><rect x="4" y="4.5" width="7" height="7" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="13" y="4.5" width="7" height="7" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="4" y="13.5" width="7" height="6" rx="2" stroke="currentColor" stroke-width="1.6"/><rect x="13" y="13.5" width="7" height="6" rx="2" stroke="currentColor" stroke-width="1.6"/></svg>Projects</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="M5 6.5h14M5 12h14M5 17.5h9" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>My tasks</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="9" cy="9" r="3.2" stroke="currentColor" stroke-width="1.6"/><path d="M3.5 19c0-3 2.5-5 5.5-5s5.5 2 5.5 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M16 7.2a3 3 0 0 1 0 5.6M17.5 19c0-2-.6-3.4-1.6-4.4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>People</a>
<a class="est-02__nl" href="#est-02"><svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="7.5" stroke="currentColor" stroke-width="1.6"/><path d="M12 8.5v4l2.5 1.8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>Activity</a>
</nav>
<div class="est-02__sfoot">
<span class="est-02__ring" id="est-02-ring" aria-hidden="true"><span class="est-02__ringtext" id="est-02-ringtext">0/3</span></span>
<span class="est-02__sfoottext">Workspace setup</span>
</div>
</aside>
<div class="est-02__main">
<header class="est-02__top">
<span class="est-02__crumb">Projects</span>
<div class="est-02__topacts">
<button class="est-02__ghost" type="button">Invite</button>
<span class="est-02__ava" aria-hidden="true">EM</span>
</div>
</header>
<div class="est-02__canvas">
<section class="est-02__setup" id="est-02-setup">
<div class="est-02__setuphead">
<h1 class="est-02__h1" id="est-02-title">Three steps and Northline is yours</h1>
<p class="est-02__sub" id="est-02-subtitle">Nothing here yet. Set the basics and your first board builds itself.</p>
</div>
<ol class="est-02__steps" id="est-02-steps">
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="1" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Name your first project</span><span class="est-02__sdesc">Boards, timeline and docs come with it.</span></span>
</li>
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="2" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Invite two teammates</span><span class="est-02__sdesc">Assignees make the board useful.</span></span>
</li>
<li class="est-02__step">
<button class="est-02__tick" type="button" data-step="3" aria-pressed="false">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"><path d="m6 12.5 4 4 8-9" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<span class="est-02__stext"><span class="est-02__sname">Connect a repository</span><span class="est-02__sdesc">Commits close tasks automatically.</span></span>
</li>
</ol>
<div class="est-02__cta">
<button class="est-02__pri" type="button" id="est-02-pri">Create your first project</button>
<button class="est-02__link" type="button" id="est-02-reset">Start over</button>
</div>
</section>
<aside class="est-02__templates">
<p class="est-02__tlabel">Start from a template</p>
<ul class="est-02__tlist">
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="sprint" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Product sprint</span><span class="est-02__tpld">Backlog, sprint, review</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="launch" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Go-to-market launch</span><span class="est-02__tpld">Assets, channels, dates</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="hiring" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Hiring pipeline</span><span class="est-02__tpld">Stages and scorecards</span></span></button></li>
<li><button class="est-02__tpl" type="button"><span class="est-02__tplg" data-g="support" aria-hidden="true"></span><span class="est-02__tpltext"><span class="est-02__tpln">Support triage</span><span class="est-02__tpld">Severity and rotation</span></span></button></li>
</ul>
</aside>
</div>
</div>
</div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap');
.est-02 {
--bg: #f7f7f5;
--surface: #ffffff;
--sunk: #f1f0ec;
--line: #e6e5e0;
--line-2: #d9d8d2;
--ink: #16161a;
--muted: #5f6068;
--faint: #8d8e95;
--accent: #2b3a67;
--ok: #2f6f4f;
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
box-sizing: border-box;
background: var(--bg);
color: var(--ink);
color-scheme: light;
font-synthesis: none;
font-family: 'Geist',ui-sans-serif,system-ui,-apple-system,sans-serif;
-webkit-font-smoothing: antialiased;
}
.est-02 *,
.est-02 *::before,
.est-02 *::after {
box-sizing: border-box;
}
.est-02 [hidden] {
display: none !important;
}
@supports (color: oklch(0.7 0.1 300)) {
.est-02 {
--bg: oklch(0.975 0.003 85);
--surface: oklch(1 0 0);
--sunk: oklch(0.955 0.005 85);
--line: oklch(0.912 0.005 85);
--line-2: oklch(0.865 0.006 85);
--ink: oklch(0.18 0.008 275);
--muted: oklch(0.48 0.011 275);
--faint: oklch(0.635 0.011 275);
--accent: oklch(0.36 0.075 265);
--ok: oklch(0.5 0.09 155);
}
}
.est-02__shell {
min-height: 100vh;
min-height: 100svh;
display: grid;
grid-template-columns: min(17rem,26vw) minmax(0,1fr);
}
.est-02__side {
display: grid;
grid-template-rows: auto minmax(0,1fr) auto;
gap: .5rem;
padding: .85rem .7rem 1rem;
border-inline-end: 1px solid var(--line);
background: var(--surface);
}
.est-02__ws {
display: grid;
grid-template-columns: auto minmax(0,1fr) auto;
gap: .6rem;
align-items: center;
text-align: start;
padding: .55rem .6rem;
cursor: pointer;
background: transparent;
border: 1px solid transparent;
border-radius: 12px;
transition: background .16s, border-color .16s;
}
.est-02__ws:hover {
background: var(--sunk);
border-color: var(--line);
}
.est-02__wsmark {
inline-size: 2.1rem;
block-size: 2.1rem;
display: grid;
place-items: center;
border-radius: 9px;
background: var(--accent);
color: #fff;
font-size: .72rem;
font-weight: 600;
letter-spacing: .02em;
}
.est-02__wstext {
min-inline-size: 0;
display: grid;
}
.est-02__wsname {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.est-02__wsplan {
font-size: .72rem;
color: var(--faint);
}
.est-02__wschev {
inline-size: 1rem;
block-size: 1rem;
color: var(--faint);
}
.est-02__nav {
display: grid;
gap: .1rem;
align-content: start;
}
.est-02__nl {
display: flex;
align-items: center;
gap: .6rem;
padding: .5rem .6rem;
border-radius: 9px;
text-decoration: none;
font-size: .875rem;
font-weight: 500;
color: var(--muted);
transition: background .16s, color .16s;
}
.est-02__nl svg {
inline-size: 1.05rem;
block-size: 1.05rem;
flex: none;
opacity: .85;
}
.est-02__nl:hover {
background: var(--sunk);
color: var(--ink);
}
.est-02__nl--on {
background: var(--sunk);
color: var(--ink);
box-shadow: inset 0 0 0 1px var(--line);
}
.est-02__sfoot {
display: flex;
align-items: center;
gap: .6rem;
padding: .7rem .6rem;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--sunk);
}
.est-02__ring {
position: relative;
inline-size: 2.15rem;
block-size: 2.15rem;
flex: none;
display: grid;
place-items: center;
border-radius: 50%;
--p: 0deg;
background: conic-gradient(var(--accent) var(--p), color-mix(in oklab, var(--line-2) 70%, transparent) 0);
transition: --p .5s ease;
}
.est-02__ring::after {
content: '';
position: absolute;
inset: 3px;
border-radius: 50%;
background: var(--sunk);
}
.est-02__ringtext {
position: relative;
font-family: 'Geist Mono',ui-monospace,monospace;
font-size: .62rem;
color: var(--muted);
}
.est-02__sfoottext {
font-size: .8rem;
color: var(--muted);
}
.est-02__main {
min-inline-size: 0;
display: grid;
grid-template-rows: auto minmax(0,1fr);
}
.est-02__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: .85rem clamp(1rem,3vw,2rem);
border-block-end: 1px solid var(--line);
background: color-mix(in oklab, var(--surface) 80%, transparent);
backdrop-filter: blur(6px);
}
.est-02__crumb {
font-size: .9rem;
font-weight: 600;
letter-spacing: -.015em;
}
.est-02__topacts {
display: flex;
align-items: center;
gap: .6rem;
}
.est-02__ghost {
min-block-size: 2.25rem;
padding: .45rem .8rem;
font: inherit;
font-size: .83rem;
font-weight: 500;
cursor: pointer;
color: var(--ink);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 9px;
transition: background .16s;
}
.est-02__ghost:hover {
background: var(--sunk);
}
.est-02__ava {
inline-size: 1.9rem;
block-size: 1.9rem;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--sunk);
border: 1px solid var(--line);
font-size: .68rem;
font-weight: 600;
color: var(--muted);
}
.est-02__canvas {
min-inline-size: 0;
padding: clamp(1.25rem,4vw,3.25rem) clamp(1rem,3vw,2rem);
display: grid;
gap: clamp(1rem,2.5vw,2rem);
grid-template-columns: repeat(auto-fit,minmax(min(19rem,100%),1fr));
align-content: start;
align-items: start;
background: radial-gradient(120% 90% at 12% 0%, color-mix(in oklab, var(--accent) 5%, transparent), transparent 60%), var(--bg);
}
.est-02__setup {
grid-column: span 2;
inline-size: min(38rem,100%);
display: grid;
gap: 1.35rem;
padding: clamp(1.25rem,3vw,2rem);
background: var(--surface);
border: 1px solid var(--line);
border-radius: 20px;
box-shadow: 0 1px 2px rgba(20,20,25,.04), 0 30px 60px -46px rgba(20,20,25,.4);
animation: est-02-rise .55s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes est-02-rise {
from {
opacity: 0;
translate: 0 10px;
}
to {
opacity: 1;
translate: 0 0;
}
}
.est-02__setuphead {
display: grid;
gap: .45rem;
}
.est-02__h1 {
margin: 0;
font-size: clamp(1.4rem,3.2vw,1.95rem);
font-weight: 600;
letter-spacing: -.032em;
line-height: 1.12;
text-wrap: balance;
}
.est-02__sub {
margin: 0;
max-inline-size: 44ch;
font-size: .95rem;
line-height: 1.55;
color: var(--muted);
text-wrap: pretty;
}
.est-02__steps {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .5rem;
}
.est-02__step {
display: grid;
grid-template-columns: auto minmax(0,1fr);
gap: .85rem;
align-items: center;
padding: .8rem .9rem;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
transition: border-color .18s, background .18s;
}
.est-02__step:hover {
border-color: var(--line-2);
background: var(--sunk);
}
.est-02__tick {
inline-size: 1.6rem;
block-size: 1.6rem;
display: grid;
place-items: center;
cursor: pointer;
flex: none;
color: transparent;
background: var(--surface);
border: 1.5px solid var(--line-2);
border-radius: 50%;
transition: background .2s, border-color .2s, color .2s, scale .2s;
}
.est-02__tick svg {
inline-size: .95rem;
block-size: .95rem;
}
.est-02__tick:hover {
border-color: var(--accent);
}
.est-02__tick[aria-pressed="true"] {
color: #fff;
background: var(--accent);
border-color: var(--accent);
scale: 1.04;
}
.est-02__stext {
min-inline-size: 0;
display: grid;
gap: .15rem;
}
.est-02__sname {
font-size: .92rem;
font-weight: 500;
letter-spacing: -.015em;
}
.est-02__step[data-done] .est-02__sname {
color: var(--muted);
text-decoration: line-through;
text-decoration-color: var(--line-2);
}
.est-02__sdesc {
font-size: .8rem;
color: var(--faint);
}
.est-02__cta {
display: flex;
flex-wrap: wrap;
gap: .75rem;
align-items: center;
}
.est-02__pri {
min-block-size: 2.85rem;
padding: .7rem 1.15rem;
font: inherit;
font-size: .9rem;
font-weight: 500;
cursor: pointer;
color: #fff;
background: var(--accent);
border: 1px solid transparent;
border-radius: 12px;
transition: translate .16s, box-shadow .18s, background .16s;
box-shadow: 0 8px 18px -12px color-mix(in oklab, var(--accent) 75%, transparent);
}
.est-02__pri:hover {
translate: 0 -1px;
box-shadow: 0 14px 26px -14px color-mix(in oklab, var(--accent) 70%, transparent);
}
.est-02__link {
padding: .4rem .1rem;
font: inherit;
font-size: .85rem;
cursor: pointer;
color: var(--muted);
background: none;
border: 0;
text-decoration: underline;
text-decoration-color: var(--line-2);
text-underline-offset: 3px;
}
.est-02__link:hover {
color: var(--ink);
}
.est-02__tick:focus-visible,
.est-02__pri:focus-visible,
.est-02__link:focus-visible,
.est-02__tpl:focus-visible,
.est-02__ghost:focus-visible,
.est-02__ws:focus-visible,
.est-02__nl:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.est-02__templates {
inline-size: min(22rem,100%);
display: grid;
gap: .6rem;
align-content: start;
}
.est-02__tlabel {
margin: 0;
font-size: .8rem;
font-weight: 500;
color: var(--faint);
}
.est-02__tlist {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: .4rem;
}
.est-02__tpl {
inline-size: 100%;
display: grid;
grid-template-columns: auto minmax(0,1fr);
gap: .7rem;
align-items: center;
text-align: start;
padding: .7rem .8rem;
cursor: pointer;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 13px;
transition: translate .16s, border-color .16s, box-shadow .18s;
}
.est-02__tpl:hover {
translate: 0 -1px;
border-color: var(--line-2);
box-shadow: 0 12px 24px -20px rgba(20,20,25,.5);
}
.est-02__tplg {
inline-size: 2rem;
block-size: 2rem;
border-radius: 8px;
background: var(--sunk);
box-shadow: inset 0 0 0 1px var(--line);
position: relative;
overflow: hidden;
}
.est-02__tplg::after {
content: '';
position: absolute;
inset: .35rem;
border-radius: 3px;
background: repeating-linear-gradient(90deg, color-mix(in oklab, var(--accent) 45%, transparent) 0 2px, transparent 2px 5px);
opacity: .7;
}
.est-02__tplg[data-g="launch"]::after {
background: conic-gradient(from 210deg, color-mix(in oklab, var(--accent) 50%, transparent), transparent 60%);
}
.est-02__tplg[data-g="hiring"]::after {
background: linear-gradient(180deg, color-mix(in oklab, var(--accent) 45%, transparent) 0 30%, transparent 30% 45%, color-mix(in oklab, var(--accent) 30%, transparent) 45% 70%, transparent 70%);
}
.est-02__tplg[data-g="support"]::after {
background: radial-gradient(circle at 50% 50%, color-mix(in oklab, var(--accent) 55%, transparent) 30%, transparent 32%);
}
.est-02__tpltext {
min-inline-size: 0;
display: grid;
gap: .1rem;
}
.est-02__tpln {
font-size: .87rem;
font-weight: 500;
letter-spacing: -.012em;
}
.est-02__tpld {
font-size: .76rem;
color: var(--faint);
}
.est-02__setup[data-ready] {
border-color: color-mix(in oklab, var(--ok) 35%, var(--line));
}
@media (max-width: 900px) {
.est-02__shell {
grid-template-columns: 1fr;
}
.est-02__side {
grid-template-rows: auto;
grid-template-columns: auto minmax(0,1fr) auto;
align-items: center;
border-inline-end: 0;
border-block-end: 1px solid var(--line);
}
.est-02__nav {
display: none;
}
.est-02__setup {
grid-column: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.est-02 *,
.est-02 *::before,
.est-02 *::after {
animation: none !important;
transition: none !important;
}
}
@media (forced-colors: active) {
.est-02__setup,
.est-02__step,
.est-02__tpl,
.est-02__pri {
border-color: CanvasText;
}
}
```
## JavaScript
```js
(function(){
var root = document.querySelector('.est-02'); if(!root) return;
var ring = root.querySelector('#est-02-ring'), ringtext = root.querySelector('#est-02-ringtext'),
title = root.querySelector('#est-02-title'), sub = root.querySelector('#est-02-subtitle'),
setup = root.querySelector('#est-02-setup'), pri = root.querySelector('#est-02-pri'),
reset = root.querySelector('#est-02-reset'), ticks = root.querySelectorAll('.est-02__tick');
var copy = [
['Three steps and Northline is yours','Nothing here yet. Set the basics and your first board builds itself.'],
['Good start','Two more and the board fills with your own work.'],
['Almost there','One step left before Northline runs itself.'],
['Workspace ready','Create the project and your team lands straight in it.']
];
function paint(){
var done = 0;
ticks.forEach(function(t){
var on = t.getAttribute('aria-pressed') === 'true';
if(on) done++;
t.closest('.est-02__step').toggleAttribute('data-done', on);
});
ring.style.setProperty('--p', (done / 3 * 360) + 'deg');
ringtext.textContent = done + '/3';
title.textContent = copy[done][0];
sub.textContent = copy[done][1];
setup.toggleAttribute('data-ready', done === 3);
pri.textContent = done === 3 ? 'Open your project' : 'Create your first project';
}
ticks.forEach(function(t){
t.addEventListener('click', function(){
t.setAttribute('aria-pressed', t.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
paint();
});
});
reset.addEventListener('click', function(){
ticks.forEach(function(t){ t.setAttribute('aria-pressed','false'); });
paint();
});
paint();
})();
```How this works
The ring is one gradient. A conic gradient stops at a custom property, an inner circle masks the middle, and the number sits on top. Three declarations, zero markup:
.est-02__ring{
--p:0deg;
background:conic-gradient(var(--accent) var(--p), var(--line-2) 0);
}
.est-02__ring::after{ inset:3px; border-radius:50%; background:var(--sunk); }ring.style.setProperty('--p', (done / 3 * 360) + 'deg');Copy is data, not markup. Four states, four pairs. The heading and the sentence are always drawn from the same row, so they cannot drift apart:
var copy = [
['Three steps and Northline is yours', 'Nothing here yet…'],
['Good start', 'Two more and the board fills…'],
['Almost there', 'One step left…'],
['Workspace ready', 'Create the project…']
];
title.textContent = copy[done][0];Toggles, not checkboxes. Each row is a button with aria-pressed, which screen readers announce as a two-state control and which needs no hidden input:
t.setAttribute('aria-pressed', t.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
t.closest('.est-02__step').toggleAttribute('data-done', on);The template rail exists because a third of new teams will not fill in a checklist at all. Giving them a copyable starting point is a better empty state than a longer list of instructions.
Make it yours
- Change the step count: the ring maths is
done / total * 360, and the copy table just needs one row per state - Persist completion to
localStorageso a returning user sees the ring where they left it - Swap the template thumbnails for real screenshots once you have them — the gradient versions are there so the demo ships without assets
- Gate the primary button until step one is done if creating an empty project is genuinely useless in your product
- Add a skip setup link: some users need the empty board more than the checklist
- Animate the ring by registering
--pwith@propertyand giving it a transition — custom properties do not interpolate otherwise
Gotchas — read before shipping
- A bare custom property cannot be transitioned. Without
@propertyandsyntax: '<angle>'the ring jumps between values - Do not use
aria-checkedon a button unless you also setrole="checkbox".aria-pressedis the right pair for a plain button - Strike-through on completed labels needs a colour of its own; inheriting the muted text colour makes it read as disabled
- Never let the heading say almost there while the ring shows one of three. One source of truth for state, always
- Onboarding checklists must be dismissible. A permanent checklist becomes furniture and stops being read
- Keep the ring out of the tab order — it is a status indicator, not a control
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), backdrop-filter, text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.
conic-gradient() needs Chrome 69 / Safari 12.1 / Firefox 83. color-mix() and oklch() are behind @supports with hex fallbacks. @property (Chrome 85 / Safari 16.4 / Firefox 128) is only needed if you want the ring to animate; without it the ring still steps correctly.