More Dashboards Snippets
Profile Completion Meter — HTML CSS JS Snippet
Profile Completion Meter · Dashboards · Plain HTML, CSS & JS · Live preview
What's included
Features
data-w weight; targetPct scores completed weight over total, so high-impact steps move the ring more.stroke-dasharray = circumference and a shrinking stroke-dashoffset draw the arc, rotated to start at the top.setRing animates the offset each requestAnimationFrame with an ease-out — smooth and export-safe, unlike a CSS stroke transition.done state with a check-circle fill and strike-through label, with hover feedback per row.message returns encouragement by threshold, turning a static label into a motivating nudge.About this UI Snippet
Profile Completion Meter — Weighted Checklist, Animated Progress Ring & Complete State

The profile completion meter is one of the most effective onboarding nudges in SaaS: a progress ring paired with a checklist of setup tasks that visibly fills as users complete each step. It leverages the Zeigarnik effect — people are driven to finish what they have started — and measurably increases activation. This snippet implements it in plain HTML, CSS, and vanilla JavaScript: a weighted task checklist, an animated SVG progress ring, an eased percentage count-up, and a celebratory complete state.
Weighted tasks, not just a count
Each task carries a data-w weight (photo 20%, verify email 20%, bio 15%, integration 20%, invite 15%, billing 10%) summing to 100. targetPct recomputes completion as the sum of completed weights over the total — so higher-impact steps move the needle more, exactly as real onboarding scores work. Tasks start with some pre-completed (photo + email = 40%), and clicking any row toggles its done state, checks it off with a strike-through, and recalculates.
Animated SVG progress ring
The ring is two SVG circles (track + progress) using the stroke-dash technique: stroke-dasharray set to the circumference (2πr) and stroke-dashoffset reduced toward zero to draw the arc, rotated -90deg to start at the top. Rather than a CSS transition on stroke-dashoffset (which utility frameworks like Tailwind do not animate, so it would snap), setRing tweens the offset every frame with a requestAnimationFrame loop and a cubic ease-out, animating smoothly from the previously shown value to the new target. The centre percentage counts up in lockstep from the same eased value.
Contextual messaging and complete state
The supporting message changes with progress — "Nice start!", "Almost there", and on 100% "Profile complete 🎉 You are all set!" — turning a static label into encouragement. At 100% the card gains a complete class that recolours the ring green and emphasises the message, marking the goal as achieved. These small reinforcements are what make completion meters convert.
Self-recomputing
Everything derives from the checklist DOM: targetPct reads the tasks each time, so adding, removing, or reweighting tasks needs no other changes, and the ring/percentage/message all follow. On load it computes the initial percentage and message from the pre-completed tasks.
Wire each task's click to actually open that setup step (or mark it done from your backend) and you have a real onboarding widget. Pair this with a stats card for account metrics, an onboarding tour for guided setup, or a streak tracker for ongoing engagement.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA "Complete your profile" card appears with a progress ring at 40% and a six-item checklist (two pre-completed).
- 2Complete a taskClick "Write a short bio" — it checks off with a strike-through and the ring animates up, the percentage counting smoothly.
- 3Watch the message changeAs you pass thresholds the supporting text updates from "Nice start!" to "Almost there".
- 4Undo a taskClick a completed task again to uncheck it — the ring eases back down and the percentage recalculates.
- 5Reach 100%Finish every task — the ring turns green and the message becomes "Profile complete 🎉 You are all set!".
- 6Wire real stepsMake each task's click open that setup flow or sync its done state from your backend; the meter recomputes automatically.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
On load, set each task's done class from your user record, then call setRing(targetPct()). In toggleTask, after toggling, PATCH the change to your API (optimistically); on failure, revert the class and recompute. Make each row's click open the actual setup step (navigate or open a modal) rather than just toggling, so the checklist drives real completion.
Edit each task's data-w. They do not need to sum to 100 — targetPct divides completed weight by the total of all weights, so any numbers work and the percentage stays correct. Weight steps by impact (e.g. connecting an integration is worth more than adding a bio) to reflect what actually matters for activation.
Utility frameworks like Tailwind do not include stroke-dashoffset in their transition utility, so a CSS-transition approach snaps in the React + Tailwind export. Updating the offset each requestAnimationFrame (with an ease-out) animates smoothly everywhere and lets the centre percentage count up in perfect sync from the same value.
Give the ring role="progressbar" with aria-valuenow/aria-valuemax updated in setRing, and make each task a real interactive element (button or checkbox) so it is keyboard-operable and announces its checked state. Announce the new percentage via an aria-live="polite" region so screen-reader users hear progress as they complete steps.
In React, hold the tasks (with done flags) in useState, derive the percentage with useMemo, and animate the ring with a small tween effect or by setting the offset from an animated value. In Vue, use a ref for tasks and a computed percentage. In Angular, track tasks on the component and bind [style.strokeDashoffset]. The stroke-dash math and checklist CSS port unchanged.
Profile Completion Meter — Export as HTML, React, Vue, Angular & Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Profile Completion Meter</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.pc-card{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:24px;width:100%;max-width:380px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
.pc-top{display:flex;align-items:center;gap:16px;margin-bottom:20px}
.pc-ring{position:relative;flex-shrink:0;width:96px;height:96px}
.pc-ring svg{transform:rotate(-90deg)}
.pc-track{fill:none;stroke:#eef2f7;stroke-width:8}
.pc-prog{fill:none;stroke:#6366f1;stroke-width:8;stroke-linecap:round;stroke-dasharray:263.9;stroke-dashoffset:263.9}
.pc-card.complete .pc-prog{stroke:#10b981}
.pc-pct{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:800;color:#1e293b;font-variant-numeric:tabular-nums}
.pc-pct small{font-size:12px;font-weight:700;color:#94a3b8;margin-left:1px}
.pc-title{font-size:16px;font-weight:800;color:#1e293b}
.pc-msg{font-size:13px;color:#64748b;margin-top:4px;line-height:1.4}
.pc-card.complete .pc-msg{color:#10b981;font-weight:700}
.pc-list{list-style:none;display:flex;flex-direction:column;gap:2px}
.pc-task{display:flex;align-items:center;gap:11px;padding:11px 10px;border-radius:10px;cursor:pointer;transition:background .15s}
.pc-task:hover{background:#f8fafc}
.pc-check{width:22px;height:22px;border-radius:50%;border:2px solid #cbd5e1;flex-shrink:0;position:relative;transition:all .18s}
.pc-task.done .pc-check{background:#10b981;border-color:#10b981}
.pc-task.done .pc-check::after{content:'';position:absolute;left:6.5px;top:3px;width:5px;height:9px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
.pc-label{flex:1;font-size:13px;font-weight:600;color:#334155;transition:color .15s}
.pc-task.done .pc-label{color:#94a3b8;text-decoration:line-through}
.pc-pts{font-size:11px;font-weight:800;color:#6366f1}
.pc-task.done .pc-pts{color:#cbd5e1}
</style>
</head>
<body>
<div class="pc-card" id="pcCard">
<div class="pc-top">
<div class="pc-ring">
<svg viewBox="0 0 100 100" width="96" height="96">
<circle class="pc-track" cx="50" cy="50" r="42"/>
<circle class="pc-prog" id="pcProg" cx="50" cy="50" r="42"/>
</svg>
<div class="pc-pct"><span id="pcPctNum">0</span><small>%</small></div>
</div>
<div class="pc-intro">
<h2 class="pc-title">Complete your profile</h2>
<p class="pc-msg" id="pcMsg">Finish setup to unlock everything.</p>
</div>
</div>
<ul class="pc-list" id="pcList">
<li class="pc-task done" data-w="20" onclick="toggleTask(this)"><span class="pc-check"></span><span class="pc-label">Add a profile photo</span><span class="pc-pts">+20%</span></li>
<li class="pc-task done" data-w="20" onclick="toggleTask(this)"><span class="pc-check"></span><span class="pc-label">Verify your email</span><span class="pc-pts">+20%</span></li>
<li class="pc-task" data-w="15" onclick="toggleTask(this)"><span class="pc-check"></span><span class="pc-label">Write a short bio</span><span class="pc-pts">+15%</span></li>
<li class="pc-task" data-w="20" onclick="toggleTask(this)"><span class="pc-check"></span><span class="pc-label">Connect an integration</span><span class="pc-pts">+20%</span></li>
<li class="pc-task" data-w="15" onclick="toggleTask(this)"><span class="pc-check"></span><span class="pc-label">Invite a teammate</span><span class="pc-pts">+15%</span></li>
<li class="pc-task" data-w="10" onclick="toggleTask(this)"><span class="pc-check"></span><span class="pc-label">Set up billing</span><span class="pc-pts">+10%</span></li>
</ul>
</div>
<script>
var C = 2 * Math.PI * 42;
var card = document.getElementById('pcCard');
var prog = document.getElementById('pcProg');
var pctNum = document.getElementById('pcPctNum');
var msgEl = document.getElementById('pcMsg');
var shown = 0;
function targetPct() {
var tasks = document.querySelectorAll('.pc-task');
var total = 0, done = 0;
tasks.forEach(function (t) {
var w = +t.dataset.w; total += w;
if (t.classList.contains('done')) done += w;
});
return total ? Math.round(done / total * 100) : 0;
}
function setRing(pct) {
var from = shown, start = performance.now();
function frame(now) {
var p = Math.min(1, (now - start) / 450);
var cur = from + (pct - from) * (1 - Math.pow(1 - p, 3));
prog.style.strokeDashoffset = (C * (1 - cur / 100)).toFixed(2);
pctNum.textContent = Math.round(cur);
if (p < 1) requestAnimationFrame(frame); else shown = pct;
}
requestAnimationFrame(frame);
}
function message(pct) {
if (pct >= 100) return 'Profile complete 🎉 You are all set!';
if (pct >= 60) return 'Almost there — just a few steps left.';
if (pct >= 30) return 'Nice start! Keep going to finish setup.';
return 'Finish setup to unlock everything.';
}
function toggleTask(el) {
el.classList.toggle('done');
var pct = targetPct();
setRing(pct);
msgEl.textContent = message(pct);
card.classList.toggle('complete', pct >= 100);
}
setRing(targetPct());
msgEl.textContent = message(targetPct());
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Profile Completion Meter</title>
<!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.pc-ring svg {
transform:rotate(-90deg)
}
.pc-card.complete .pc-prog {
stroke:#10b981
}
.pc-pct small {
font-size:12px;font-weight:700;color:#94a3b8;margin-left:1px
}
.pc-card.complete .pc-msg {
color:#10b981;font-weight:700
}
.pc-task.done .pc-check {
background:#10b981;border-color:#10b981
}
.pc-task.done .pc-check::after {
content:'';position:absolute;left:6.5px;top:3px;width:5px;height:9px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)
}
.pc-task.done .pc-label {
color:#94a3b8;text-decoration:line-through
}
.pc-task.done .pc-pts {
color:#cbd5e1
}
</style>
</head>
<body>
<div class="pc-card bg-[#fff] border border-[#e2e8f0] rounded-[18px] p-6 w-full max-w-[380px] shadow-[0_14px_44px_rgba(15,23,42,.07)]" id="pcCard">
<div class="flex items-center gap-4 mb-5">
<div class="pc-ring relative shrink-0 w-24 h-24">
<svg viewBox="0 0 100 100" width="96" height="96">
<circle class="fill-none stroke-[#eef2f7] [stroke-width:8]" cx="50" cy="50" r="42"/>
<circle class="pc-prog fill-none stroke-[#6366f1] [stroke-width:8] [stroke-linecap:round] [stroke-dasharray:263.9] [stroke-dashoffset:263.9]" id="pcProg" cx="50" cy="50" r="42"/>
</svg>
<div class="pc-pct absolute inset-0 flex items-center justify-center text-[22px] font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]"><span id="pcPctNum">0</span><small>%</small></div>
</div>
<div class="pc-intro">
<h2 class="text-base font-extrabold text-[#1e293b]">Complete your profile</h2>
<p class="pc-msg text-[13px] text-[#64748b] mt-1 leading-[1.4]" id="pcMsg">Finish setup to unlock everything.</p>
</div>
</div>
<ul class="[list-style:none] flex flex-col gap-0.5" id="pcList">
<li class="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc] done" data-w="20" onclick="toggleTask(this)"><span class="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span class="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Add a profile photo</span><span class="pc-pts text-[11px] font-extrabold text-[#6366f1]">+20%</span></li>
<li class="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc] done" data-w="20" onclick="toggleTask(this)"><span class="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span class="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Verify your email</span><span class="pc-pts text-[11px] font-extrabold text-[#6366f1]">+20%</span></li>
<li class="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc]" data-w="15" onclick="toggleTask(this)"><span class="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span class="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Write a short bio</span><span class="pc-pts text-[11px] font-extrabold text-[#6366f1]">+15%</span></li>
<li class="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc]" data-w="20" onclick="toggleTask(this)"><span class="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span class="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Connect an integration</span><span class="pc-pts text-[11px] font-extrabold text-[#6366f1]">+20%</span></li>
<li class="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc]" data-w="15" onclick="toggleTask(this)"><span class="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span class="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Invite a teammate</span><span class="pc-pts text-[11px] font-extrabold text-[#6366f1]">+15%</span></li>
<li class="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc]" data-w="10" onclick="toggleTask(this)"><span class="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span class="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Set up billing</span><span class="pc-pts text-[11px] font-extrabold text-[#6366f1]">+10%</span></li>
</ul>
</div>
<script>
var C = 2 * Math.PI * 42;
var card = document.getElementById('pcCard');
var prog = document.getElementById('pcProg');
var pctNum = document.getElementById('pcPctNum');
var msgEl = document.getElementById('pcMsg');
var shown = 0;
function targetPct() {
var tasks = document.querySelectorAll('.pc-task');
var total = 0, done = 0;
tasks.forEach(function (t) {
var w = +t.dataset.w; total += w;
if (t.classList.contains('done')) done += w;
});
return total ? Math.round(done / total * 100) : 0;
}
function setRing(pct) {
var from = shown, start = performance.now();
function frame(now) {
var p = Math.min(1, (now - start) / 450);
var cur = from + (pct - from) * (1 - Math.pow(1 - p, 3));
prog.style.strokeDashoffset = (C * (1 - cur / 100)).toFixed(2);
pctNum.textContent = Math.round(cur);
if (p < 1) requestAnimationFrame(frame); else shown = pct;
}
requestAnimationFrame(frame);
}
function message(pct) {
if (pct >= 100) return 'Profile complete 🎉 You are all set!';
if (pct >= 60) return 'Almost there — just a few steps left.';
if (pct >= 30) return 'Nice start! Keep going to finish setup.';
return 'Finish setup to unlock everything.';
}
function toggleTask(el) {
el.classList.toggle('done');
var pct = targetPct();
setRing(pct);
msgEl.textContent = message(pct);
card.classList.toggle('complete', pct >= 100);
}
setRing(targetPct());
msgEl.textContent = message(targetPct());
</script>
</body>
</html>import React, { useEffect } from 'react';
// CSS — optionally move to ProfileCompletionMeter.module.css
const css = `
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.pc-card{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:24px;width:100%;max-width:380px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
.pc-top{display:flex;align-items:center;gap:16px;margin-bottom:20px}
.pc-ring{position:relative;flex-shrink:0;width:96px;height:96px}
.pc-ring svg{transform:rotate(-90deg)}
.pc-track{fill:none;stroke:#eef2f7;stroke-width:8}
.pc-prog{fill:none;stroke:#6366f1;stroke-width:8;stroke-linecap:round;stroke-dasharray:263.9;stroke-dashoffset:263.9}
.pc-card.complete .pc-prog{stroke:#10b981}
.pc-pct{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:800;color:#1e293b;font-variant-numeric:tabular-nums}
.pc-pct small{font-size:12px;font-weight:700;color:#94a3b8;margin-left:1px}
.pc-title{font-size:16px;font-weight:800;color:#1e293b}
.pc-msg{font-size:13px;color:#64748b;margin-top:4px;line-height:1.4}
.pc-card.complete .pc-msg{color:#10b981;font-weight:700}
.pc-list{list-style:none;display:flex;flex-direction:column;gap:2px}
.pc-task{display:flex;align-items:center;gap:11px;padding:11px 10px;border-radius:10px;cursor:pointer;transition:background .15s}
.pc-task:hover{background:#f8fafc}
.pc-check{width:22px;height:22px;border-radius:50%;border:2px solid #cbd5e1;flex-shrink:0;position:relative;transition:all .18s}
.pc-task.done .pc-check{background:#10b981;border-color:#10b981}
.pc-task.done .pc-check::after{content:'';position:absolute;left:6.5px;top:3px;width:5px;height:9px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
.pc-label{flex:1;font-size:13px;font-weight:600;color:#334155;transition:color .15s}
.pc-task.done .pc-label{color:#94a3b8;text-decoration:line-through}
.pc-pts{font-size:11px;font-weight:800;color:#6366f1}
.pc-task.done .pc-pts{color:#cbd5e1}
`;
export default function ProfileCompletionMeter() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var C = 2 * Math.PI * 42;
var card = document.getElementById('pcCard');
var prog = document.getElementById('pcProg');
var pctNum = document.getElementById('pcPctNum');
var msgEl = document.getElementById('pcMsg');
var shown = 0;
function targetPct() {
var tasks = document.querySelectorAll('.pc-task');
var total = 0, done = 0;
tasks.forEach(function (t) {
var w = +t.dataset.w; total += w;
if (t.classList.contains('done')) done += w;
});
return total ? Math.round(done / total * 100) : 0;
}
function setRing(pct) {
var from = shown, start = performance.now();
function frame(now) {
var p = Math.min(1, (now - start) / 450);
var cur = from + (pct - from) * (1 - Math.pow(1 - p, 3));
prog.style.strokeDashoffset = (C * (1 - cur / 100)).toFixed(2);
pctNum.textContent = Math.round(cur);
if (p < 1) requestAnimationFrame(frame); else shown = pct;
}
requestAnimationFrame(frame);
}
function message(pct) {
if (pct >= 100) return 'Profile complete 🎉 You are all set!';
if (pct >= 60) return 'Almost there — just a few steps left.';
if (pct >= 30) return 'Nice start! Keep going to finish setup.';
return 'Finish setup to unlock everything.';
}
function toggleTask(el) {
el.classList.toggle('done');
var pct = targetPct();
setRing(pct);
msgEl.textContent = message(pct);
card.classList.toggle('complete', pct >= 100);
}
setRing(targetPct());
msgEl.textContent = message(targetPct());
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof toggleTask === 'function') window.toggleTask = toggleTask;
}, []);
return (
<>
<style>{css}</style>
<div className="pc-card" id="pcCard">
<div className="pc-top">
<div className="pc-ring">
<svg viewBox="0 0 100 100" width="96" height="96">
<circle className="pc-track" cx="50" cy="50" r="42"/>
<circle className="pc-prog" id="pcProg" cx="50" cy="50" r="42"/>
</svg>
<div className="pc-pct"><span id="pcPctNum">0</span><small>%</small></div>
</div>
<div className="pc-intro">
<h2 className="pc-title">Complete your profile</h2>
<p className="pc-msg" id="pcMsg">Finish setup to unlock everything.</p>
</div>
</div>
<ul className="pc-list" id="pcList">
<li className="pc-task done" data-w="20" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check"></span><span className="pc-label">Add a profile photo</span><span className="pc-pts">+20%</span></li>
<li className="pc-task done" data-w="20" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check"></span><span className="pc-label">Verify your email</span><span className="pc-pts">+20%</span></li>
<li className="pc-task" data-w="15" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check"></span><span className="pc-label">Write a short bio</span><span className="pc-pts">+15%</span></li>
<li className="pc-task" data-w="20" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check"></span><span className="pc-label">Connect an integration</span><span className="pc-pts">+20%</span></li>
<li className="pc-task" data-w="15" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check"></span><span className="pc-label">Invite a teammate</span><span className="pc-pts">+15%</span></li>
<li className="pc-task" data-w="10" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check"></span><span className="pc-label">Set up billing</span><span className="pc-pts">+10%</span></li>
</ul>
</div>
</>
);
}import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+
export default function ProfileCompletionMeter() {
// Auto-generated escape hatch: the original snippet's vanilla JS runs once
// after mount and queries the rendered DOM. For idiomatic React, lift this
// into state + handlers.
useEffect(() => {
const _listeners = [];
const _originalAddEventListener = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function(type, listener, options) {
_listeners.push({ target: this, type, listener, options });
return _originalAddEventListener.call(this, type, listener, options);
};
var C = 2 * Math.PI * 42;
var card = document.getElementById('pcCard');
var prog = document.getElementById('pcProg');
var pctNum = document.getElementById('pcPctNum');
var msgEl = document.getElementById('pcMsg');
var shown = 0;
function targetPct() {
var tasks = document.querySelectorAll('.pc-task');
var total = 0, done = 0;
tasks.forEach(function (t) {
var w = +t.dataset.w; total += w;
if (t.classList.contains('done')) done += w;
});
return total ? Math.round(done / total * 100) : 0;
}
function setRing(pct) {
var from = shown, start = performance.now();
function frame(now) {
var p = Math.min(1, (now - start) / 450);
var cur = from + (pct - from) * (1 - Math.pow(1 - p, 3));
prog.style.strokeDashoffset = (C * (1 - cur / 100)).toFixed(2);
pctNum.textContent = Math.round(cur);
if (p < 1) requestAnimationFrame(frame); else shown = pct;
}
requestAnimationFrame(frame);
}
function message(pct) {
if (pct >= 100) return 'Profile complete 🎉 You are all set!';
if (pct >= 60) return 'Almost there — just a few steps left.';
if (pct >= 30) return 'Nice start! Keep going to finish setup.';
return 'Finish setup to unlock everything.';
}
function toggleTask(el) {
el.classList.toggle('done');
var pct = targetPct();
setRing(pct);
msgEl.textContent = message(pct);
card.classList.toggle('complete', pct >= 100);
}
setRing(targetPct());
msgEl.textContent = message(targetPct());
// Cleanup: restore addEventListener and remove all listeners
return () => {
EventTarget.prototype.addEventListener = _originalAddEventListener;
for (const { target, type, listener, options } of _listeners) {
target.removeEventListener(type, listener, options);
}
};
// Expose handlers used by inline JSX events to the global scope
if (typeof toggleTask === 'function') window.toggleTask = toggleTask;
}, []);
return (
<>
<style>{`
* {
box-sizing:border-box;margin:0;padding:0
}
body {
font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px
}
.pc-ring svg {
transform:rotate(-90deg)
}
.pc-card.complete .pc-prog {
stroke:#10b981
}
.pc-pct small {
font-size:12px;font-weight:700;color:#94a3b8;margin-left:1px
}
.pc-card.complete .pc-msg {
color:#10b981;font-weight:700
}
.pc-task.done .pc-check {
background:#10b981;border-color:#10b981
}
.pc-task.done .pc-check::after {
content:'';position:absolute;left:6.5px;top:3px;width:5px;height:9px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)
}
.pc-task.done .pc-label {
color:#94a3b8;text-decoration:line-through
}
.pc-task.done .pc-pts {
color:#cbd5e1
}
`}</style>
<div className="pc-card bg-[#fff] border border-[#e2e8f0] rounded-[18px] p-6 w-full max-w-[380px] shadow-[0_14px_44px_rgba(15,23,42,.07)]" id="pcCard">
<div className="flex items-center gap-4 mb-5">
<div className="pc-ring relative shrink-0 w-24 h-24">
<svg viewBox="0 0 100 100" width="96" height="96">
<circle className="fill-none stroke-[#eef2f7] [stroke-width:8]" cx="50" cy="50" r="42"/>
<circle className="pc-prog fill-none stroke-[#6366f1] [stroke-width:8] [stroke-linecap:round] [stroke-dasharray:263.9] [stroke-dashoffset:263.9]" id="pcProg" cx="50" cy="50" r="42"/>
</svg>
<div className="pc-pct absolute inset-0 flex items-center justify-center text-[22px] font-extrabold text-[#1e293b] [font-variant-numeric:tabular-nums]"><span id="pcPctNum">0</span><small>%</small></div>
</div>
<div className="pc-intro">
<h2 className="text-base font-extrabold text-[#1e293b]">Complete your profile</h2>
<p className="pc-msg text-[13px] text-[#64748b] mt-1 leading-[1.4]" id="pcMsg">Finish setup to unlock everything.</p>
</div>
</div>
<ul className="[list-style:none] flex flex-col gap-0.5" id="pcList">
<li className="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc] done" data-w="20" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span className="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Add a profile photo</span><span className="pc-pts text-[11px] font-extrabold text-[#6366f1]">+20%</span></li>
<li className="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc] done" data-w="20" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span className="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Verify your email</span><span className="pc-pts text-[11px] font-extrabold text-[#6366f1]">+20%</span></li>
<li className="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc]" data-w="15" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span className="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Write a short bio</span><span className="pc-pts text-[11px] font-extrabold text-[#6366f1]">+15%</span></li>
<li className="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc]" data-w="20" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span className="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Connect an integration</span><span className="pc-pts text-[11px] font-extrabold text-[#6366f1]">+20%</span></li>
<li className="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc]" data-w="15" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span className="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Invite a teammate</span><span className="pc-pts text-[11px] font-extrabold text-[#6366f1]">+15%</span></li>
<li className="pc-task flex items-center gap-[11px] py-[11px] px-2.5 rounded-[10px] cursor-pointer [transition:background_.15s] hover:bg-[#f8fafc]" data-w="10" onClick={(event) => { toggleTask(event.currentTarget) }}><span className="pc-check w-[22px] h-[22px] rounded-full border-2 border-[#cbd5e1] shrink-0 relative [transition:all_.18s]"></span><span className="pc-label flex-1 text-[13px] font-semibold text-[#334155] [transition:color_.15s]">Set up billing</span><span className="pc-pts text-[11px] font-extrabold text-[#6366f1]">+10%</span></li>
</ul>
</div>
</>
);
}<template>
<div class="pc-card" id="pcCard">
<div class="pc-top">
<div class="pc-ring">
<svg viewBox="0 0 100 100" width="96" height="96">
<circle class="pc-track" cx="50" cy="50" r="42"/>
<circle class="pc-prog" id="pcProg" cx="50" cy="50" r="42"/>
</svg>
<div class="pc-pct"><span id="pcPctNum">0</span><small>%</small></div>
</div>
<div class="pc-intro">
<h2 class="pc-title">Complete your profile</h2>
<p class="pc-msg" id="pcMsg">Finish setup to unlock everything.</p>
</div>
</div>
<ul class="pc-list" id="pcList">
<li class="pc-task done" data-w="20" @click="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Add a profile photo</span><span class="pc-pts">+20%</span></li>
<li class="pc-task done" data-w="20" @click="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Verify your email</span><span class="pc-pts">+20%</span></li>
<li class="pc-task" data-w="15" @click="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Write a short bio</span><span class="pc-pts">+15%</span></li>
<li class="pc-task" data-w="20" @click="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Connect an integration</span><span class="pc-pts">+20%</span></li>
<li class="pc-task" data-w="15" @click="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Invite a teammate</span><span class="pc-pts">+15%</span></li>
<li class="pc-task" data-w="10" @click="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Set up billing</span><span class="pc-pts">+10%</span></li>
</ul>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
var C = 2 * Math.PI * 42;
let card;
let prog;
let pctNum;
let msgEl;
var shown = 0;
function targetPct() {
var tasks = document.querySelectorAll('.pc-task');
var total = 0, done = 0;
tasks.forEach(function (t) {
var w = +t.dataset.w; total += w;
if (t.classList.contains('done')) done += w;
});
return total ? Math.round(done / total * 100) : 0;
}
function setRing(pct) {
var from = shown, start = performance.now();
function frame(now) {
var p = Math.min(1, (now - start) / 450);
var cur = from + (pct - from) * (1 - Math.pow(1 - p, 3));
prog.style.strokeDashoffset = (C * (1 - cur / 100)).toFixed(2);
pctNum.textContent = Math.round(cur);
if (p < 1) requestAnimationFrame(frame); else shown = pct;
}
requestAnimationFrame(frame);
}
function message(pct) {
if (pct >= 100) return 'Profile complete 🎉 You are all set!';
if (pct >= 60) return 'Almost there — just a few steps left.';
if (pct >= 30) return 'Nice start! Keep going to finish setup.';
return 'Finish setup to unlock everything.';
}
function toggleTask(el) {
el.classList.toggle('done');
var pct = targetPct();
setRing(pct);
msgEl.textContent = message(pct);
card.classList.toggle('complete', pct >= 100);
}
onMounted(() => {
card = document.getElementById('pcCard');
prog = document.getElementById('pcProg');
pctNum = document.getElementById('pcPctNum');
msgEl = document.getElementById('pcMsg');
setRing(targetPct());
msgEl.textContent = message(targetPct());
});
</script>
<style scoped>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.pc-card{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:24px;width:100%;max-width:380px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
.pc-top{display:flex;align-items:center;gap:16px;margin-bottom:20px}
.pc-ring{position:relative;flex-shrink:0;width:96px;height:96px}
.pc-ring svg{transform:rotate(-90deg)}
.pc-track{fill:none;stroke:#eef2f7;stroke-width:8}
.pc-prog{fill:none;stroke:#6366f1;stroke-width:8;stroke-linecap:round;stroke-dasharray:263.9;stroke-dashoffset:263.9}
.pc-card.complete .pc-prog{stroke:#10b981}
.pc-pct{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:800;color:#1e293b;font-variant-numeric:tabular-nums}
.pc-pct small{font-size:12px;font-weight:700;color:#94a3b8;margin-left:1px}
.pc-title{font-size:16px;font-weight:800;color:#1e293b}
.pc-msg{font-size:13px;color:#64748b;margin-top:4px;line-height:1.4}
.pc-card.complete .pc-msg{color:#10b981;font-weight:700}
.pc-list{list-style:none;display:flex;flex-direction:column;gap:2px}
.pc-task{display:flex;align-items:center;gap:11px;padding:11px 10px;border-radius:10px;cursor:pointer;transition:background .15s}
.pc-task:hover{background:#f8fafc}
.pc-check{width:22px;height:22px;border-radius:50%;border:2px solid #cbd5e1;flex-shrink:0;position:relative;transition:all .18s}
.pc-task.done .pc-check{background:#10b981;border-color:#10b981}
.pc-task.done .pc-check::after{content:'';position:absolute;left:6.5px;top:3px;width:5px;height:9px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
.pc-label{flex:1;font-size:13px;font-weight:600;color:#334155;transition:color .15s}
.pc-task.done .pc-label{color:#94a3b8;text-decoration:line-through}
.pc-pts{font-size:11px;font-weight:800;color:#6366f1}
.pc-task.done .pc-pts{color:#cbd5e1}
</style>// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-profile-completion-meter',
standalone: true,
imports: [CommonModule],
encapsulation: ViewEncapsulation.None,
template: `
<div class="pc-card" id="pcCard">
<div class="pc-top">
<div class="pc-ring">
<svg viewBox="0 0 100 100" width="96" height="96">
<circle class="pc-track" cx="50" cy="50" r="42"/>
<circle class="pc-prog" id="pcProg" cx="50" cy="50" r="42"/>
</svg>
<div class="pc-pct"><span id="pcPctNum">0</span><small>%</small></div>
</div>
<div class="pc-intro">
<h2 class="pc-title">Complete your profile</h2>
<p class="pc-msg" id="pcMsg">Finish setup to unlock everything.</p>
</div>
</div>
<ul class="pc-list" id="pcList">
<li class="pc-task done" data-w="20" (click)="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Add a profile photo</span><span class="pc-pts">+20%</span></li>
<li class="pc-task done" data-w="20" (click)="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Verify your email</span><span class="pc-pts">+20%</span></li>
<li class="pc-task" data-w="15" (click)="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Write a short bio</span><span class="pc-pts">+15%</span></li>
<li class="pc-task" data-w="20" (click)="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Connect an integration</span><span class="pc-pts">+20%</span></li>
<li class="pc-task" data-w="15" (click)="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Invite a teammate</span><span class="pc-pts">+15%</span></li>
<li class="pc-task" data-w="10" (click)="toggleTask($event.currentTarget)"><span class="pc-check"></span><span class="pc-label">Set up billing</span><span class="pc-pts">+10%</span></li>
</ul>
</div>
`,
styles: [`
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#f1f5f9;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.pc-card{background:#fff;border:1px solid #e2e8f0;border-radius:18px;padding:24px;width:100%;max-width:380px;box-shadow:0 14px 44px rgba(15,23,42,.07)}
.pc-top{display:flex;align-items:center;gap:16px;margin-bottom:20px}
.pc-ring{position:relative;flex-shrink:0;width:96px;height:96px}
.pc-ring svg{transform:rotate(-90deg)}
.pc-track{fill:none;stroke:#eef2f7;stroke-width:8}
.pc-prog{fill:none;stroke:#6366f1;stroke-width:8;stroke-linecap:round;stroke-dasharray:263.9;stroke-dashoffset:263.9}
.pc-card.complete .pc-prog{stroke:#10b981}
.pc-pct{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:800;color:#1e293b;font-variant-numeric:tabular-nums}
.pc-pct small{font-size:12px;font-weight:700;color:#94a3b8;margin-left:1px}
.pc-title{font-size:16px;font-weight:800;color:#1e293b}
.pc-msg{font-size:13px;color:#64748b;margin-top:4px;line-height:1.4}
.pc-card.complete .pc-msg{color:#10b981;font-weight:700}
.pc-list{list-style:none;display:flex;flex-direction:column;gap:2px}
.pc-task{display:flex;align-items:center;gap:11px;padding:11px 10px;border-radius:10px;cursor:pointer;transition:background .15s}
.pc-task:hover{background:#f8fafc}
.pc-check{width:22px;height:22px;border-radius:50%;border:2px solid #cbd5e1;flex-shrink:0;position:relative;transition:all .18s}
.pc-task.done .pc-check{background:#10b981;border-color:#10b981}
.pc-task.done .pc-check::after{content:'';position:absolute;left:6.5px;top:3px;width:5px;height:9px;border:solid #fff;border-width:0 2px 2px 0;transform:rotate(45deg)}
.pc-label{flex:1;font-size:13px;font-weight:600;color:#334155;transition:color .15s}
.pc-task.done .pc-label{color:#94a3b8;text-decoration:line-through}
.pc-pts{font-size:11px;font-weight:800;color:#6366f1}
.pc-task.done .pc-pts{color:#cbd5e1}
`]
})
export class ProfileCompletionMeterComponent implements AfterViewInit {
ngAfterViewInit(): void {
var C = 2 * Math.PI * 42;
var card = document.getElementById('pcCard');
var prog = document.getElementById('pcProg');
var pctNum = document.getElementById('pcPctNum');
var msgEl = document.getElementById('pcMsg');
var shown = 0;
function targetPct() {
var tasks = document.querySelectorAll('.pc-task');
var total = 0, done = 0;
tasks.forEach(function (t) {
var w = +t.dataset.w; total += w;
if (t.classList.contains('done')) done += w;
});
return total ? Math.round(done / total * 100) : 0;
}
function setRing(pct) {
var from = shown, start = performance.now();
function frame(now) {
var p = Math.min(1, (now - start) / 450);
var cur = from + (pct - from) * (1 - Math.pow(1 - p, 3));
prog.style.strokeDashoffset = (C * (1 - cur / 100)).toFixed(2);
pctNum.textContent = Math.round(cur);
if (p < 1) requestAnimationFrame(frame); else shown = pct;
}
requestAnimationFrame(frame);
}
function message(pct) {
if (pct >= 100) return 'Profile complete 🎉 You are all set!';
if (pct >= 60) return 'Almost there — just a few steps left.';
if (pct >= 30) return 'Nice start! Keep going to finish setup.';
return 'Finish setup to unlock everything.';
}
function toggleTask(el) {
el.classList.toggle('done');
var pct = targetPct();
setRing(pct);
msgEl.textContent = message(pct);
card.classList.toggle('complete', pct >= 100);
}
setRing(targetPct());
msgEl.textContent = message(targetPct());
// Bind inline-handler functions to the component so the template can call them
Object.assign(this, { targetPct, setRing, message, toggleTask });
}
}