Grow an audience
A follow button toggles between “Follow” and “Following,” usually paired with a follower count. Show the two states clearly.
HTML
<button class="follow" aria-pressed="false">Follow</button> <button class="follow following" aria-pressed="true">Following</button> <span class="count"><b>2,481</b> followers</span>
CSS
.follow {
border-radius: 999px; padding: 9px 18px; font-weight: 700;
background: #38bdf8; color: #fff; border: none;
}
.follow.following { /* toggled state */
background: #fff; color: #111; border: 1px solid #cbd5e1;
}
JavaScript
// Toggle Follow / Following with a live count, persisted per user.
// HTML expected:
// <button class="follow" data-user="ada">Follow</button>
// <span class="count"><b id="followers">2,481</b> followers</span>
document.querySelectorAll('.follow').forEach(function (btn) {
var user = btn.dataset.user || 'default';
var key = 'following:' + user;
var saved = localStorage.getItem(key) === '1';
// restore saved state on load
if (saved) {
btn.classList.add('following');
btn.setAttribute('aria-pressed', 'true');
btn.textContent = 'Following';
}
btn.addEventListener('click', function () {
var nowFollowing = !btn.classList.contains('following');
btn.classList.toggle('following', nowFollowing);
btn.setAttribute('aria-pressed', String(nowFollowing));
btn.textContent = nowFollowing ? 'Following' : 'Follow';
localStorage.setItem(key, nowFollowing ? '1' : '0');
// bump follower count up/down
var counter = document.getElementById('followers');
if (counter) {
var n = parseInt(counter.textContent.replace(/,/g, ''), 10) || 0;
counter.textContent = (n + (nowFollowing ? 1 : -1)).toLocaleString();
}
});
});
Let visitors talk back
Each comment shows an avatar, name, timestamp, the message, and actions (like, reply). A simple flex row per comment.
HTML
<article class="comment">
<img class="avatar" src="sam.jpg" alt="">
<div>
<p><b>Sam R.</b> <time>· 2h ago</time></p>
<p>This tutorial finally made flexbox click for me. Thank you!</p>
<div class="actions"><button>♥ 12</button><button>Reply</button></div>
</div>
</article>
<article class="comment">
<img class="avatar" src="dana.jpg" alt="">
<div>
<p><b>Dana K.</b> <time>· 1h ago</time></p>
<p>Agreed — the live demos make all the difference.</p>
<div class="actions"><button>♥ 4</button><button>Reply</button></div>
</div>
</article>
CSS
.comment { display: flex; gap: 12px; }
.avatar { width: 38px; height: 38px; border-radius: 50%; }
.actions { display: flex; gap: 14px; font-size: .8rem; }
JavaScript
// Comments: post new, toggle like, reveal reply form.
// HTML expected: a <form id="commentForm"> with <input name="body">
// and a container <div class="comments">...</div>
var commentForm = document.getElementById('commentForm');
var commentsList = document.querySelector('.comments');
if (commentForm && commentsList) {
commentForm.addEventListener('submit', function (e) {
e.preventDefault();
var body = commentForm.body.value.trim();
if (!body) return;
var html =
'<article class="comment">' +
'<img class="avatar" src="you.jpg" alt="">' +
'<div>' +
'<p><b>You</b> <time>· just now</time></p>' +
'<p>' + body.replace(/</g,'<') + '</p>' +
'<div class="actions">' +
'<button class="like">♥ 0</button>' +
'<button class="reply">Reply</button>' +
'</div>' +
'</div>' +
'</article>';
commentsList.insertAdjacentHTML('afterbegin', html);
commentForm.reset();
});
}
// Like button: toggle .liked + increment/decrement count
commentsList && commentsList.addEventListener('click', function (e) {
var like = e.target.closest('.like');
if (like) {
var liked = like.classList.toggle('liked');
var n = parseInt(like.textContent.replace(/\D/g, ''), 10) || 0;
like.innerHTML = '♥ ' + (n + (liked ? 1 : -1));
return;
}
// Reply button: reveal an inline reply form
var reply = e.target.closest('.reply');
if (reply) {
var comment = reply.closest('.comment');
var existing = comment.querySelector('.reply-form');
if (existing) { existing.remove(); return; }
var form = document.createElement('form');
form.className = 'reply-form';
form.innerHTML = '<input name="r" placeholder="Write a reply…"><button>Post</button>';
comment.appendChild(form);
form.r.focus();
}
});
Toggle preferences
A settings list pairs each option with a switch. Stack rows with a label, a short description, and the toggle on the right.
HTML
<div class="setting">
<div>
<p class="label">Email notifications</p>
<p class="sub">Get updates about your account</p>
</div>
<button class="switch on" role="switch" aria-checked="true"></button>
</div>
<div class="setting">
<div>
<p class="label">Public profile</p>
<p class="sub">Anyone can view your profile</p>
</div>
<button class="switch" role="switch" aria-checked="false"></button>
</div>
CSS
.setting { display: flex; justify-content: space-between; align-items: center; }
.switch { width: 44px; height: 26px; border-radius: 999px; background: #cbd5e1; position: relative; }
.switch::after { content: ""; position: absolute; top: 3px; left: 3px;
width: 20px; height: 20px; border-radius: 50%; background: #fff; }
.switch.on { background: #22c55e; } .switch.on::after { left: 21px; }
JavaScript
// Toggle switches save to localStorage; "Save" shows a "Settings saved" toast.
document.querySelectorAll('.setting .switch').forEach(function (sw) {
var key = 'pref:' + (sw.dataset.key || sw.previousElementSibling?.querySelector('.label')?.textContent || Math.random());
// restore
if (localStorage.getItem(key) === '1') {
sw.classList.add('on');
sw.setAttribute('aria-checked', 'true');
}
sw.addEventListener('click', function () {
var on = sw.classList.toggle('on');
sw.setAttribute('aria-checked', String(on));
localStorage.setItem(key, on ? '1' : '0');
});
});
// Simple toast for the Save button
function showToast(msg) {
var t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);' +
'background:#111;color:#fff;padding:10px 18px;border-radius:8px;font-size:.9rem;z-index:9999;';
document.body.appendChild(t);
setTimeout(function () { t.remove(); }, 1800);
}
var settingsForm = document.getElementById('settingsForm');
settingsForm && settingsForm.addEventListener('submit', function (e) {
e.preventDefault();
showToast('Settings saved');
});
A timeline of what happened
A vertical feed of events, each with an icon, a short description, and a timestamp — connected by a line to read as a stream.
HTML
<ul class="feed">
<li><span class="icon">♥</span>
<div><p><b>Sam</b> liked your post</p><time>2h ago</time></div></li>
<li><span class="icon">+</span>
<div><p><b>Dana</b> started following you</p><time>5h ago</time></div></li>
<li><span class="icon">💬</span>
<div><p><b>Lee</b> commented on your photo</p><time>1d ago</time></div></li>
</ul>
CSS
.feed li { display: flex; gap: 12px; padding: 12px 0; position: relative; }
.feed li::before { /* the connecting line */
content: ""; position: absolute; left: 18px; top: 40px; bottom: -12px;
width: 2px; background: #e5e7eb;
}
.icon { width: 38px; height: 38px; border-radius: 50%; display: grid; place-items: center; }
JavaScript
// "Load more" appends ~3 simulated feed items; time-ago strings auto-refresh.
var feed = document.querySelector('.feed');
var loadMore = document.getElementById('loadMore');
var fakeItems = [
{ icon: '♥', text: '<b>Mia</b> liked your post', ts: Date.now() - 60 * 60 * 3 * 1000 },
{ icon: '+', text: '<b>Jordan</b> started following you', ts: Date.now() - 60 * 60 * 8 * 1000 },
{ icon: '💬', text: '<b>Ada</b> commented on your photo', ts: Date.now() - 60 * 60 * 26 * 1000 }
];
function timeAgo(ms) {
var s = Math.max(1, Math.floor((Date.now() - ms) / 1000));
if (s < 60) return s + 's ago';
if (s < 3600) return Math.floor(s / 60) + 'm ago';
if (s < 86400) return Math.floor(s / 3600) + 'h ago';
return Math.floor(s / 86400) + 'd ago';
}
loadMore && loadMore.addEventListener('click', function () {
fakeItems.forEach(function (it) {
var li = document.createElement('li');
li.dataset.ts = it.ts;
li.innerHTML = '<span class="icon">' + it.icon + '</span>' +
'<div><p>' + it.text + '</p>' +
'<time>' + timeAgo(it.ts) + '</time></div>';
feed.appendChild(li);
});
});
// auto-refresh every minute
setInterval(function () {
feed.querySelectorAll('li[data-ts] time').forEach(function (t) {
t.textContent = timeAgo(+t.closest('li').dataset.ts);
});
}, 60000);
A list of discussion threads
Each thread row shows a title, who started it, and a reply count. Keep the title prominent and the reply count aligned right.
HTML
<ul class="forum">
<li class="thread">
<div>
<p class="title">How do I center a div in 2026?</p>
<p class="by">by Ada · 3d ago</p>
</div>
<span class="replies">24</span>
</li>
<li class="thread">
<div>
<p class="title">Best free fonts for body text?</p>
<p class="by">by Lee · 1d ago</p>
</div>
<span class="replies">9</span>
</li>
<li class="thread">
<div>
<p class="title">Show your portfolio — feedback welcome</p>
<p class="by">by Mia · 6h ago</p>
</div>
<span class="replies">41</span>
</li>
</ul>
CSS
.thread { display: flex; align-items: center; gap: 12px;
padding: 12px 14px; border-bottom: 1px solid #f1f5f9; }
.replies { margin-left: auto; font-weight: 700; } /* push count right */
JavaScript
// Forum voting: .vote-up / .vote-down update a .score; .mark-solved toggles a ribbon.
document.querySelectorAll('.thread').forEach(function (thread) {
var score = thread.querySelector('.score');
thread.addEventListener('click', function (e) {
if (e.target.closest('.vote-up') && score) {
score.textContent = (parseInt(score.textContent, 10) || 0) + 1;
} else if (e.target.closest('.vote-down') && score) {
score.textContent = (parseInt(score.textContent, 10) || 0) - 1;
} else if (e.target.closest('.mark-solved')) {
thread.classList.toggle('solved');
var ribbon = thread.querySelector('.solved-ribbon');
if (thread.classList.contains('solved') && !ribbon) {
var r = document.createElement('span');
r.className = 'solved-ribbon';
r.textContent = 'Solved';
r.style.cssText = 'background:#22c55e;color:#fff;padding:2px 8px;border-radius:6px;font-size:.7rem;margin-left:8px;';
thread.querySelector('.title').appendChild(r);
} else if (ribbon) {
ribbon.remove();
}
}
});
});
A messaging bubble
Incoming and outgoing messages sit on opposite sides with different colors. An input row sits pinned at the bottom.
HTML
<div class="chat">
<div class="head">Support</div>
<div class="messages">
<p class="bubble them">Hi! How can we help?</p>
<p class="bubble me">My order hasn't shipped yet.</p>
<p class="bubble them">Let me check that for you…</p>
</div>
<form class="input"><input placeholder="Type a message…"><button>Send</button></form>
</div>
CSS
.chat { border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; }
.head { background: #0b1a33; color: #fff; padding: 10px 14px; font-weight: 700; }
.messages { padding: 14px; display: grid; gap: 8px; }
.bubble { max-width: 75%; padding: 8px 12px; border-radius: 14px; }
.bubble.them { background: #eef1f5; } /* left */
.bubble.me { background: #38bdf8; color: #fff; margin-left: auto; } /* right */
JavaScript
// Chat: open panel on bubble click, post message + simulated bot reply, close panel.
var chatBubble = document.querySelector('.chat-bubble');
var chatPanel = document.querySelector('.chat');
var chatClose = document.querySelector('.chat .close');
var chatForm = document.querySelector('.chat .input');
var chatInput = chatForm && chatForm.querySelector('input');
var chatBody = document.querySelector('.chat .messages');
chatBubble && chatBubble.addEventListener('click', function () {
chatPanel.classList.add('open');
});
chatClose && chatClose.addEventListener('click', function () {
chatPanel.classList.remove('open');
});
chatForm && chatForm.addEventListener('submit', function (e) {
e.preventDefault();
var text = chatInput.value.trim();
if (!text) return;
var mine = document.createElement('p');
mine.className = 'bubble me';
mine.textContent = text;
chatBody.appendChild(mine);
chatInput.value = '';
chatBody.scrollTop = chatBody.scrollHeight;
// Simulated bot reply after 600ms
setTimeout(function () {
var reply = document.createElement('p');
reply.className = 'bubble them';
reply.textContent = 'Let me check that for you…';
chatBody.appendChild(reply);
chatBody.scrollTop = chatBody.scrollHeight;
}, 600);
});
The whole thing as one file
Every pattern above — share, follow, comments, settings, activity feed, forum, and chat — combined into one complete HTML file: a small community hub. Copy it into a new file called community.html, open it in a browser, and it just works — no libraries, no build step. Here's the live result, then the full code.
Live preview
Complete HTML — copy this whole file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Community Hub</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f6f7fb; color: #1f2433; line-height: 1.6; padding: 24px; }
.wrap { max-width: 760px; margin: 0 auto; display: grid; gap: 20px; }
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 14px; padding: 22px; }
.card h2 { font-size: 1.05rem; margin-bottom: 14px; }
.page-head { text-align: center; padding: 8px 0 4px; }
.page-head h1 { font-size: 1.8rem; letter-spacing: -.02em; }
.page-head p { color: #6b7280; font-size: .92rem; }
/* ===== Share buttons ===== */
.share { display: flex; gap: 10px; flex-wrap: wrap; }
.share a { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px;
border-radius: 8px; color: #fff; text-decoration: none; font-size: .85rem; font-weight: 600; }
.share .x { background: #111; }
.share .fb { background: #1877f2; }
.share .li { background: #0a66c2; }
.share .wa { background: #25d366; }
/* ===== Follow ===== */
.follow-row { display: inline-flex; align-items: center; gap: 12px; }
.follow { display: inline-flex; align-items: center; gap: 7px; background: #38bdf8; color: #fff;
border: none; border-radius: 999px; padding: 9px 18px; font-weight: 700; cursor: pointer; font-size: .88rem; }
.follow.following { background: #fff; color: #111; border: 1px solid #cbd5e1; }
.count { font-size: .85rem; color: #6b7280; }
.count b { color: #111; }
/* ===== Comments ===== */
.comments { display: grid; gap: 14px; }
.comment { display: flex; gap: 12px; }
.avatar { width: 38px; height: 38px; border-radius: 50%; flex: none;
background: linear-gradient(135deg,#38bdf8,#9b7bff); }
.comment .nm { font-weight: 700; color: #111; font-size: .85rem; }
.comment .meta { color: #9aa3b2; font-size: .72rem; }
.comment p { color: #374151; font-size: .88rem; margin: 3px 0; }
.comment .act { display: flex; gap: 14px; font-size: .75rem; color: #6b7280; }
.comment .act button { background: none; border: none; color: #6b7280; cursor: pointer; font-size: .75rem; }
/* ===== Settings ===== */
.setting { display: flex; align-items: center; justify-content: space-between;
padding: 12px 0; border-bottom: 1px solid #eef1f5; }
.setting:last-child { border-bottom: none; }
.setting .lbl { font-size: .9rem; color: #111; font-weight: 600; }
.setting .sub { font-size: .78rem; color: #6b7280; }
.switch { width: 44px; height: 26px; border-radius: 999px; background: #cbd5e1; border: none;
position: relative; flex: none; cursor: pointer; transition: background .2s; }
.switch::after { content: ""; position: absolute; top: 3px; left: 3px; width: 20px; height: 20px;
border-radius: 50%; background: #fff; transition: left .2s; }
.switch.on { background: #22c55e; }
.switch.on::after { left: 21px; }
/* ===== Activity feed ===== */
.feed { list-style: none; display: grid; gap: 0; }
.feed li { display: flex; gap: 12px; padding: 12px 0; position: relative; }
.feed li::before { content: ""; position: absolute; left: 18px; top: 40px; bottom: -12px;
width: 2px; background: #e5e7eb; }
.feed li:last-child::before { display: none; }
.feed .icon { width: 38px; height: 38px; border-radius: 50%; background: #e0f2fe; color: #0369a1;
display: grid; place-items: center; flex: none; font-size: 1rem; }
.feed .tx { font-size: .88rem; color: #374151; }
.feed .tx b { color: #111; }
.feed time { font-size: .72rem; color: #9aa3b2; }
/* ===== Forum ===== */
.forum { list-style: none; }
.thread { display: flex; align-items: center; gap: 12px; padding: 12px 0; border-bottom: 1px solid #f1f5f9; }
.thread:last-child { border-bottom: none; }
.thread .title { font-weight: 600; color: #111; font-size: .9rem; }
.thread .by { font-size: .75rem; color: #9aa3b2; }
.replies { margin-left: auto; font-weight: 700; color: #0369a1; font-size: .85rem; }
/* ===== Chat ===== */
.chat { border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; max-width: 360px; }
.chat .head { background: #0b1a33; color: #fff; padding: 10px 14px; font-weight: 700; font-size: .88rem; }
.messages { padding: 14px; display: grid; gap: 8px; min-height: 90px; }
.bubble { max-width: 75%; padding: 8px 12px; border-radius: 14px; font-size: .85rem; }
.bubble.them { background: #eef1f5; color: #111; border-bottom-left-radius: 4px; }
.bubble.me { background: #38bdf8; color: #fff; margin-left: auto; border-bottom-right-radius: 4px; }
.chat .input { display: flex; gap: 8px; padding: 10px 12px; border-top: 1px solid #eef1f5; }
.chat .input input { flex: 1; border: 1px solid #cbd5e1; border-radius: 999px; padding: 8px 12px; font-size: .85rem; }
.chat .input button { background: #38bdf8; color: #fff; border: none; border-radius: 999px; padding: 0 16px; cursor: pointer; font-weight: 600; }
</style>
</head>
<body>
<div class="wrap">
<div class="page-head">
<h1>DevTown Community</h1>
<p>Share, follow, chat, and join the conversation.</p>
</div>
<section class="card">
<h2>Spread the word</h2>
<div class="share">
<a class="x" href="https://twitter.com/intent/tweet?url=PAGE_URL">Share</a>
<a class="fb" href="https://www.facebook.com/sharer/sharer.php?u=PAGE_URL">Share</a>
<a class="li" href="https://www.linkedin.com/sharing/share-offsite/?url=PAGE_URL">Share</a>
<a class="wa" href="https://wa.me/?text=PAGE_URL">Share</a>
</div>
<div class="follow-row" style="margin-top:16px;">
<button class="follow" aria-pressed="false">+ Follow</button>
<span class="count"><b id="followers">2,481</b> followers</span>
</div>
</section>
<section class="card">
<h2>Comments</h2>
<div class="comments">
<article class="comment">
<span class="avatar"></span>
<div>
<span class="nm">Sam R.</span> <span class="meta">· 2h ago</span>
<p>This community finally made flexbox click for me. Thank you!</p>
<div class="act"><button>♥ 12</button><button>Reply</button></div>
</div>
</article>
<article class="comment">
<span class="avatar"></span>
<div>
<span class="nm">Dana K.</span> <span class="meta">· 1h ago</span>
<p>Agreed — the live demos make all the difference.</p>
<div class="act"><button>♥ 4</button><button>Reply</button></div>
</div>
</article>
</div>
</section>
<section class="card">
<h2>Settings</h2>
<div class="setting">
<div>
<p class="lbl">Email notifications</p>
<p class="sub">Get updates about your account</p>
</div>
<button class="switch on" role="switch" aria-checked="true"></button>
</div>
<div class="setting">
<div>
<p class="lbl">Public profile</p>
<p class="sub">Anyone can view your profile</p>
</div>
<button class="switch" role="switch" aria-checked="false"></button>
</div>
</section>
<section class="card">
<h2>Recent activity</h2>
<ul class="feed">
<li><span class="icon">♥</span>
<div><p class="tx"><b>Sam</b> liked your post</p><time>2h ago</time></div></li>
<li><span class="icon">+</span>
<div><p class="tx"><b>Dana</b> started following you</p><time>5h ago</time></div></li>
<li><span class="icon">💬</span>
<div><p class="tx"><b>Lee</b> commented on your photo</p><time>1d ago</time></div></li>
</ul>
</section>
<section class="card">
<h2>Discussion threads</h2>
<ul class="forum">
<li class="thread">
<div><p class="title">How do I center a div in 2026?</p><p class="by">by Ada · 3d ago</p></div>
<span class="replies">24 replies</span>
</li>
<li class="thread">
<div><p class="title">Best free fonts for body text?</p><p class="by">by Lee · 1d ago</p></div>
<span class="replies">9 replies</span>
</li>
<li class="thread">
<div><p class="title">Show your portfolio — feedback welcome</p><p class="by">by Mia · 6h ago</p></div>
<span class="replies">41 replies</span>
</li>
</ul>
</section>
<section class="card">
<h2>Live chat</h2>
<div class="chat">
<div class="head">Support</div>
<div class="messages" id="messages">
<p class="bubble them">Hi! How can we help?</p>
<p class="bubble me">My order hasn't shipped yet.</p>
<p class="bubble them">Let me check that for you…</p>
</div>
<form class="input" id="chatForm">
<input id="chatInput" placeholder="Type a message…" aria-label="Message">
<button type="submit">Send</button>
</form>
</div>
</section>
</div>
<script>
// Follow toggle with live count
var followBtn = document.querySelector('.follow');
var followers = document.getElementById('followers');
var base = 2481, following = false;
followBtn.addEventListener('click', function () {
following = !following;
followBtn.classList.toggle('following', following);
followBtn.setAttribute('aria-pressed', following);
followBtn.textContent = following ? '✓ Following' : '+ Follow';
followers.textContent = (base + (following ? 1 : 0)).toLocaleString();
});
// Settings switches
document.querySelectorAll('.switch').forEach(function (sw) {
sw.addEventListener('click', function () {
var on = sw.classList.toggle('on');
sw.setAttribute('aria-checked', on);
});
});
// Chat: send a message
var form = document.getElementById('chatForm');
var input = document.getElementById('chatInput');
var messages = document.getElementById('messages');
form.addEventListener('submit', function (e) {
e.preventDefault();
var text = input.value.trim();
if (!text) return;
var bubble = document.createElement('p');
bubble.className = 'bubble me';
bubble.textContent = text;
messages.appendChild(bubble);
input.value = '';
messages.scrollTop = messages.scrollHeight;
setTimeout(function () {
var reply = document.createElement('p');
reply.className = 'bubble them';
reply.textContent = 'Thanks! A team member will reply shortly.';
messages.appendChild(reply);
messages.scrollTop = messages.scrollHeight;
}, 700);
});
</script>
</body>
</html>
Keep building
See Testimonials, Profile Picture, and Icons — or browse the full Component Library.
This tutorial finally made flexbox click for me. Thank you!
Agreed — the live demos make all the difference.