A long technical article can contain excellent information and still feel completely passive. The reader scrolls, reads a few sections, closes the tab, and has no practical way to track what they have already completed.
That is exactly the problem an interactive checklist can solve.
At SwiftyKits, we use small interactive elements such as progress-saving checklists to turn certain tutorials into something readers can actually work through. Instead of simply reading an SEO audit checklist, for example, a visitor can mark each task complete and see their progress update immediately.
The interesting part is that you do not need React, Vue, a WordPress plugin, a database, or a user account to build it.
A combination of semantic HTML, CSS, Vanilla JavaScript, and the browser's localStorage API is enough.
In this tutorial, we will build the complete system from scratch: clickable checklist items, completed visual states, dynamic progress percentages, persistent browser storage, and a reset function.
<input>change listener updates class<progress> & % textdata-step-id arrayWhy Add Interactivity to a Blog Post?
Most blog posts are static documents. An interactive checklist changes the experience: the visitor can complete step 1, click the next checkbox, watch the progress increase from 20% to 40%, leave the page, and return later with their previous progress still available.
Google itself considers interactive features such as calculators, tools, games, and search functionality to be potential parts of a page's main content when they directly serve the page's purpose.
The goal should not be: "Add JavaScript because Google likes interactive pages."
Building the HTML, CSS, and DOM Foundation
1. Start With Semantic HTML and Stable data-step-id Attributes
Use a real <input type="checkbox"> inside a <label> and a native <progress> element rather than recreating form controls with generic <div> elements:
<section class="checklist" aria-labelledby="checklist-title">
<div class="checklist-header">
<h2 id="checklist-title">Technical SEO Checklist</h2>
<p>
Completed:
<strong id="progress-text">0%</strong>
</p>
<progress
id="progress-bar"
value="0"
max="100"
aria-label="Checklist progress">
0%
</progress>
</div>
<ol class="checklist-steps">
<li class="checklist-step" data-step-id="https">
<label>
<input type="checkbox" class="step-checkbox">
<span>Check the site's HTTPS configuration.</span>
</label>
</li>
<li class="checklist-step" data-step-id="internal-links">
<label>
<input type="checkbox" class="step-checkbox">
<span>Review broken internal links.</span>
</label>
</li>
<li class="checklist-step" data-step-id="canonicals">
<label>
<input type="checkbox" class="step-checkbox">
<span>Verify canonical URLs.</span>
</label>
</li>
</ol>
<button type="button" id="reset-checklist">
Reset Checklist
</button>
</section>Giving each step a stable data-step-id (like "https" or "canonicals") prevents saved state from breaking if you reorder or insert checklist items later.
2. Style the Completed State With CSS
JavaScript will toggle a .step-completed class whenever the user checks an item:
.checklist-step {
padding: 16px;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 12px;
transition: opacity 0.2s ease, background-color 0.2s ease;
}
.checklist-step label {
display: flex;
align-items: flex-start;
gap: 12px;
cursor: pointer;
}
.step-checkbox {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.checklist-step.step-completed {
opacity: 0.65;
background: #f5f5f5;
}
.checklist-step.step-completed span {
text-decoration: line-through;
}Native <input type="checkbox">, <label>, and <progress>.
Styles .step-completed opacity and strikethrough text.
Listens for change events and calculates percentages.
Stores serialized JSON array across browser sessions.
3. Select the Checklist Elements With JavaScript
Connect the DOM elements to JavaScript references:
const checkboxes = document.querySelectorAll(".step-checkbox");
const steps = document.querySelectorAll(".checklist-step");
const progressText = document.getElementById("progress-text");
const progressBar = document.getElementById("progress-bar");
const resetButton = document.getElementById("reset-checklist");4. Calculate the Progress Percentage
Use the formula completed steps ÷ total steps × 100 with a defensive check against dividing by zero:
function updateProgress() {
const totalSteps = checkboxes.length;
const completedSteps = document.querySelectorAll(
".step-checkbox:checked"
).length;
const percentage = totalSteps === 0
? 0
: Math.round((completedSteps / totalSteps) * 100);
progressText.textContent = `${percentage}%`;
progressBar.value = percentage;
}5. Toggle the .step-completed Class
Use the second boolean argument of classList.toggle() to add or remove the class cleanly:
function updateStepState(checkbox) {
const step = checkbox.closest(".checklist-step");
if (!step) {
return;
}
step.classList.toggle(
"step-completed",
checkbox.checked
);
}6. Add the Checkbox Event Listener
Listen for the change event on every checkbox:
checkboxes.forEach((checkbox) => {
checkbox.addEventListener("change", () => {
updateStepState(checkbox);
updateProgress();
});
});Adding localStorage Persistence
7. Define a Unique localStorage Key
The Web Storage API provides localStorage.setItem(key, value), localStorage.getItem(key), and localStorage.removeItem(key). Define a unique key per article or checklist:
const STORAGE_KEY = "swiftykits-seo-checklist";8. Save Completed Steps with JSON.stringify()
Collect the data-step-id of every checked item into an array and serialize it to a JSON string:
function saveProgress() {
const completedSteps = [];
steps.forEach((step) => {
const checkbox = step.querySelector(".step-checkbox");
if (checkbox.checked) {
completedSteps.push(step.dataset.stepId);
}
});
localStorage.setItem(
STORAGE_KEY,
JSON.stringify(completedSteps)
);
}9. Load the Saved Progress on Page Load
Retrieve the stored string, parse it with JSON.parse(), and restore the checked and visual states:
function loadProgress() {
const savedData = localStorage.getItem(STORAGE_KEY);
if (!savedData) {
return;
}
const completedSteps = JSON.parse(savedData);
steps.forEach((step) => {
const stepId = step.dataset.stepId;
const checkbox = step.querySelector(".step-checkbox");
if (completedSteps.includes(stepId)) {
checkbox.checked = true;
step.classList.add("step-completed");
}
});
updateProgress();
}10. Save Immediately When the User Checks a Step
Call saveProgress() inside your change listener:
checkboxes.forEach((checkbox) => {
checkbox.addEventListener("change", () => {
updateStepState(checkbox);
updateProgress();
saveProgress();
});
});11. Add a Scoped Reset Button (Use removeItem, Not clear)
Use localStorage.removeItem(STORAGE_KEY) rather than localStorage.clear() so you don't wipe out unrelated site data:
resetButton.addEventListener("click", () => {
localStorage.removeItem(STORAGE_KEY);
checkboxes.forEach((checkbox) => {
checkbox.checked = false;
});
steps.forEach((step) => {
step.classList.remove("step-completed");
});
updateProgress();
});12. Wrap JSON.parse() in try...catch for Production Safety
Here is the complete production-ready script with defensive error handling:
const STORAGE_KEY = "swiftykits-seo-checklist";
const checkboxes = document.querySelectorAll(".step-checkbox");
const steps = document.querySelectorAll(".checklist-step");
const progressText = document.getElementById("progress-text");
const progressBar = document.getElementById("progress-bar");
const resetButton = document.getElementById("reset-checklist");
function updateStepState(checkbox) {
const step = checkbox.closest(".checklist-step");
if (!step) return;
step.classList.toggle("step-completed", checkbox.checked);
}
function updateProgress() {
const totalSteps = checkboxes.length;
const completedSteps = document.querySelectorAll(".step-checkbox:checked").length;
const percentage = totalSteps === 0 ? 0 : Math.round((completedSteps / totalSteps) * 100);
progressText.textContent = `${percentage}%`;
progressBar.value = percentage;
}
function saveProgress() {
const completedSteps = [];
steps.forEach((step) => {
const checkbox = step.querySelector(".step-checkbox");
if (checkbox.checked) {
completedSteps.push(step.dataset.stepId);
}
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(completedSteps));
}
function loadProgress() {
const savedData = localStorage.getItem(STORAGE_KEY);
if (!savedData) return;
try {
const completedSteps = JSON.parse(savedData);
if (!Array.isArray(completedSteps)) return;
steps.forEach((step) => {
const checkbox = step.querySelector(".step-checkbox");
const stepId = step.dataset.stepId;
if (completedSteps.includes(stepId)) {
checkbox.checked = true;
step.classList.add("step-completed");
}
});
updateProgress();
} catch (error) {
console.warn("Could not restore checklist progress.", error);
}
}
checkboxes.forEach((checkbox) => {
checkbox.addEventListener("change", () => {
updateStepState(checkbox);
updateProgress();
saveProgress();
});
});
resetButton.addEventListener("click", () => {
localStorage.removeItem(STORAGE_KEY);
checkboxes.forEach((checkbox) => { checkbox.checked = false; });
steps.forEach((step) => { step.classList.remove("step-completed"); });
updateProgress();
});
loadProgress();Accessibility, Storage Scope & Testing
13. Add Accessibility and Polite Live Regions
Ensure every checkbox has an associated label, visible keyboard focus, and non-color-only completion indicators (such as strikethrough text). You can also add an aria-live="polite" status element for screen readers:
<p id="progress-status" aria-live="polite">
0% complete
</p>14. Understand localStorage Origin and Device Limitations
localStorage belongs to the specific browser and origin—it does not sync across devices or browsers without a backend database.
Persists across page reloads, tab closures, and browser restarts on this device.
Isolated device storage unless connected to an authenticated backend database.
Each browser engine maintains its own local storage bucket per origin.
15. Run the 7 Developer QA Tests
- Basic interaction: Check items and confirm
.step-completedand percentage updates. - Reload: Complete three steps, refresh the page, and confirm persistence.
- Browser restart: Close and reopen the browser to verify session persistence.
- Reset: Click Reset Checklist and verify everything returns to 0%.
- Invalid storage: Inject malformed JSON in DevTools and confirm graceful failure via
try...catch. - Keyboard: Navigate and toggle checkboxes using
TabandSpace. - Mobile: Verify full-width
<label>tap targets on small screens.
16. Scale the Data Model Only When Needed
If you later need timestamps or metadata per step, you can store an object instead of an array:
const progress = {
"https": {
completed: true,
completedAt: "2026-10-08"
}
};Start with the smallest data model that solves the actual problem.
17. Keep the Feature Directly Tied to the Article's Purpose
Avoid decorative JS bloat. An interactive checklist works when it turns a 15-point technical SEO audit, a Google Ads account audit, or a website launch guide into an actionable working document.
18. Verify the 16-Point Production Checklist
data-step-id.step-completed<progress>localStorage.setItem()localStorage.getItem()try...catchConclusion
An interactive blog checklist does not require a complicated JavaScript framework or backend infrastructure:
The browser handles the persistence, JavaScript handles the state, CSS handles the visual feedback, and HTML provides the underlying structure and accessibility.
change EventclassList.toggle()progressBar.valueJSON.stringify()localStorage.setItem()
localStorage.getItem()try { JSON.parse() }completedSteps.includes()checkbox.checked = trueupdateProgress()
If you were adding one persistent interactive component to your own technical blog today, what process would be most useful for readers to complete directly on the page?