Comment section HTML template with CSS and JavaScript
Copy this free template in 3 parts, HTML, CSS and JavaScript, or skip the code with CommentCamp, a hosted embed that's $0 for site owners. The template saves comments with localStorage, which holds up to 5 MiB per origin in each visitor's browser, so other visitors can't see them until you add a backend.
- CommentCamp embed
- $0 for site owners
- Files
- 3: HTML, CSS, JS
- localStorage limit
- 5 MiB per origin
- Workers free tier
- 100,000 requests/day
What this template does
This template gives you a labeled form for a name and a comment, an error message area, a Post comment button and a list that shows posted comments on the page. It needs no libraries, no build tools and no server.
JavaScript reads each submission with FormData and saves it in the visitor's own browser with localStorage, so comments survive a page reload and a browser restart.
The catch: localStorage only exists on the device of the person typing, so other visitors won't see those comments. That's fine for prototypes and layout mockups; a live comment section needs shared storage, covered below.
Step 1: Add the HTML
Paste this markup where the comment section should appear. It wraps a heading, the form and an ordered list in a section element.
<section class="comments" aria-labelledby="comments-title">
<h2 id="comments-title">Comments</h2>
<form id="comment-form" class="comment-form">
<div class="field">
<label for="comment-author">Name</label>
<input id="comment-author" name="author" type="text" autocomplete="name" maxlength="60" required>
</div>
<div class="field">
<label for="comment-message">Comment</label>
<textarea id="comment-message" name="message" rows="4" maxlength="2000" required></textarea>
</div>
<div class="field honeypot" aria-hidden="true">
<label for="comment-website">Website</label>
<input id="comment-website" name="website" type="text" tabindex="-1" autocomplete="off">
</div>
<p id="comment-error" class="comment-error" role="alert" hidden></p>
<button type="submit">Post comment</button>
</form>
<ol id="comment-list" class="comment-list" aria-live="polite"></ol>
</section>
<script src="comments.js" defer></script>
- Every label's
forattribute exactly matches theidof its field, as the W3C WAI forms tutorial recommends, so screen readers announce the right label. - The name and comment fields are
requiredand capped at 60 and 2,000 characters. - A hidden "website" field works as a honeypot. People never see it, simple bots often fill it in, and the script drops any submission where it isn't empty. It's also hidden from screen readers with
aria-hiddenand skipped by the Tab key withtabindex="-1". - The list uses
aria-live="polite", so screen readers announce new comments without interrupting. MDN reservesassertivefor updates where interrupting the user is necessary.
Step 2: Style it with CSS
Add these rules to your stylesheet. They space out the fields, style the button and turn each comment into a simple card.
.comments {
max-width: 40rem;
margin: 3rem auto;
font: 1rem/1.6 system-ui, sans-serif;
color: #1f2328;
}
.comment-form {
display: grid;
gap: 1rem;
}
.field {
display: grid;
gap: 0.375rem;
}
.field label {
font-weight: 600;
}
.field input,
.field textarea {
font: inherit;
padding: 0.625rem 0.75rem;
border: 1px solid #8c959f;
border-radius: 6px;
}
.comment-form button {
justify-self: start;
font: inherit;
font-weight: 600;
padding: 0.625rem 1.25rem;
border: 0;
border-radius: 6px;
background: #0b57d0;
color: #fff;
cursor: pointer;
}
.field input:focus-visible,
.field textarea:focus-visible,
.comment-form button:focus-visible {
outline: 3px solid #0b57d0;
outline-offset: 2px;
}
.honeypot {
position: absolute;
left: -9999px;
}
.comment-error {
margin: 0;
color: #b42318;
}
.comment-list {
display: grid;
gap: 1rem;
margin: 2rem 0 0;
padding: 0;
list-style: none;
}
.comment {
padding: 1rem 1.25rem;
border: 1px solid #d0d7de;
border-radius: 8px;
}
.comment-meta {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: baseline;
margin: 0 0 0.25rem;
font-size: 0.875rem;
color: #57606a;
}
.comment-meta strong {
font-size: 1rem;
color: #1f2328;
}
.comment-text {
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
Every field and the button get a visible 3-pixel focus outline, so keyboard users always know where they are. Comment text keeps the line breaks the commenter typed, long words and links wrap instead of breaking your layout, and the honeypot field is moved off screen.
Step 3: Make it work with JavaScript
Save this as comments.js next to your page. The defer attribute on the script tag runs it after the page's HTML has been parsed.
const STORAGE_KEY = `comments:${location.pathname}`;
const form = document.getElementById('comment-form');
const list = document.getElementById('comment-list');
const errorBox = document.getElementById('comment-error');
function loadComments() {
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '[]');
if (!Array.isArray(saved)) return [];
return saved.filter(
(c) => typeof c?.author === 'string' && typeof c?.message === 'string' && typeof c?.createdAt === 'string'
);
} catch (error) {
console.error('Could not read saved comments', error);
return [];
}
}
function saveComments(comments) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(comments));
return true;
} catch (error) {
console.error('Could not save the comment', error);
return false;
}
}
function renderComment(comment) {
const item = document.createElement('li');
item.className = 'comment';
const meta = document.createElement('p');
meta.className = 'comment-meta';
const author = document.createElement('strong');
author.textContent = comment.author;
const time = document.createElement('time');
time.dateTime = comment.createdAt;
time.textContent = new Date(comment.createdAt).toLocaleString();
const text = document.createElement('p');
text.className = 'comment-text';
text.textContent = comment.message;
meta.append(author, time);
item.append(meta, text);
return item;
}
function showError(message) {
errorBox.textContent = message;
errorBox.hidden = message === '';
}
form.addEventListener('submit', (event) => {
event.preventDefault();
showError('');
const data = new FormData(form);
// Bots tend to fill every field, so we silently drop honeypot hits
if (String(data.get('website') ?? '') !== '') return;
const author = String(data.get('author') ?? '').trim();
const message = String(data.get('message') ?? '').trim();
if (author === '' || message === '') {
showError('Please add your name and a comment.');
return;
}
const comment = { author, message, createdAt: new Date().toISOString() };
if (!saveComments([...loadComments(), comment])) {
showError('Your browser could not save this comment.');
return;
}
list.append(renderComment(comment));
form.reset();
});
list.append(...loadComments().map(renderComment));
- Comments are built with
createElementandtextContent, neverinnerHTML. MDN says: "Node.textContent should be used when you know that the user-provided content should be plain text. This prevents it being parsed as HTML." The OWASP cheat sheet agrees: "Try to refactor your code to remove references to unsafe sinks like innerHTML, and instead use textContent or value." - Reading and saving sit in
tryandcatchblocks. If storage is full, the browser throws aQuotaExceededErrorand the script shows an error message instead of failing silently; corrupted saved data is ignored instead of breaking the page. - Comments are stored under one key per page path, so comments on one article don't show up on every page of your site.
- Honeypot hits are dropped without an error message, so bots get no feedback to learn from.
Note: MDN puts the limit at up to 5 MiB of local storage per origin. Plain-text comments are small, so one visitor won't hit it in normal use.
Why static HTML can't save comments for everyone
HTML only describes the page. MDN notes that localStorage data "is saved across browser sessions", but it's kept per origin inside each visitor's own browser.
So when you post a test comment, your browser saves it and shows it back after a reload, which makes the form look finished. Open the page in another browser, or ask a friend to load it, and the list is empty.
Readers do look for that discussion: in a 2016 Center for Media Engagement survey of 1,471 Americans, 77.9% had read online comments at some point. A shared comment section needs a server that stores comments in a database and serves them through an API, plus spam protection and moderation.
Where to store comments for real
These are the common ways to store comments outside the browser, with free-tier limits as of October 2026.
| Storage option | What it needs | Free tier or cost | Spam handling |
|---|---|---|---|
| Hosted embed like CommentCamp | One script tag on your page | $0 for site owners with CommentCamp; others vary | Usually built in: spam filtering and a moderation queue |
localStorage (this template) |
Nothing beyond the visitor's browser | Free, up to 5 MiB per origin | Honeypot only; comments aren't shared |
| Formspree form backend | Your form's action pointed at Formspree | Free plan: 50 submissions/month | You review entries and publish them by hand |
| Cloudflare Workers + D1 | A Worker API plus a D1 database | Free: 100,000 requests/day, 5 GB D1 storage | You build the honeypot, rate limits and moderation |
| Firebase Cloud Firestore | Firestore plus your own read and write logic | Spark: 1 GiB stored, 20,000 writes/day | You build the honeypot, rate limits and moderation |
| Supabase | A hosted database and API | Free: 500 MB database, paused after 1 idle week | You build the honeypot, rate limits and moderation |
Tip: A form backend like Formspree collects submissions but doesn't display them on your page, so you still copy approved comments into your site yourself.
What a comment backend needs
If you build your own on Cloudflare Workers and D1, Firebase or Supabase, plan for:
- A database table for comments.
- A POST endpoint that validates input against the same 60- and 2,000-character limits and stores plain text.
- A GET endpoint that returns approved comments only.
- A honeypot check on the server, since anyone can skip your JavaScript and post directly.
- A rate limit per IP address, so one address can't flood a thread.
- A moderation queue for new comments. For comparison, WordPress holds any comment with 2 or more links for approval by default.
Our guide to comment spam covers more ways to keep bots out.
When a hosted comment embed is the better call
A database, an API, rate limits and a moderation queue are real ongoing work. If you just want readers talking under your posts, a hosted embed you paste in once is simpler. Compare all 3 routes in our guide to adding a comment section to any website, or see comments for static sites if your site is plain HTML.
We're building CommentCamp for independent publishers, bloggers and WordPress sites. It's $0 for site owners, free forever with no hidden tiers, and readers never pay to read or comment; optional member Pro (cosmetic badges, profile upgrades and eligible XP boosts) funds it, not ads.
It launches with a universal embed script for any HTML or CMS-backed site, an official WordPress plugin, unlimited comments, replies and threads, automated spam filtering and a moderation queue, with full data exports on the roadmap. CommentCamp is in private early access: join the early-access waitlist and we'll email your invite as rollout expands.
Frequently asked questions
Can I add comments to a website with just HTML?
You can build the comment form and layout with HTML and CSS, and JavaScript can save entries in each visitor's own browser. Plain HTML can't share those comments with other visitors, though. To show comments to everyone, you need a server with a database or a hosted comment embed.
Is localStorage OK for website comments?
Only for prototypes or for testing a layout on your own device. localStorage holds up to 5 MiB per origin inside each visitor's browser, so nobody else sees what was posted. A real comment section needs a server with a database or a hosted embed.
How do I stop spam on an HTML comment form?
Add a hidden honeypot field that people never see and discard any submission that fills it in. On the server, rate limit how many comments one IP address can post in a short window, and hold new comments, or comments with 2 or more links, in a moderation queue.
What is a free backend for website comments?
CommentCamp is the no-build option: a hosted embed that's free for site owners, with spam filtering built in, in private early access. To build your own, use the free tiers of Cloudflare Workers with D1 (100,000 requests a day, 5 GB storage), Firebase Cloud Firestore (1 GiB stored) or Supabase (500 MB database).
How do I make an HTML comment box accessible?
Give every input and textarea a label whose for attribute matches the field's id, keep a visible focus outline for keyboard users, and mark the comment list as a polite live region so screen readers announce new comments without interrupting the reader.
Sources
- developer.mozilla.org/en-US/docs/Web/API/Window/localStorage
- developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_...
- developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
- cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_P...
- w3.org/WAI/tutorials/forms/labels
- developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Refer...
- developers.cloudflare.com/workers/platform/limits
- developers.cloudflare.com/d1/platform/pricing
- firebase.google.com/pricing
- supabase.com/pricing
- formspree.io/plans
- wordpress.org/documentation/article/settings-discussion-screen
- mediaengagement.org/research/survey-of-commenters-and-comment...
Prices and plan limits checked on October 30, 2026.