UI Components Easy
Interactive Comment Box
A clean, expandable comment input component with user avatar, character limits, and smooth focus states.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--cb-border: rgba(255, 255, 255, 0.1);
--cb-focus: #3b82f6;
--cb-focus-glow: rgba(59, 130, 246, 0.2);
--cb-text: #f8fafc;
--cb-muted: #64748b;
--cb-bg: rgba(255, 255, 255, 0.04);
--cb-btn-cancel-hover: rgba(255, 255, 255, 0.06);
--cb-body-bg: #0f1117;
}
body {
background: var(--cb-body-bg);
color: var(--cb-text);
font-family: "Inter", system-ui, sans-serif;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem 1rem;
}
.comment-box-widget {
display: flex;
gap: 1rem;
max-width: 600px;
width: 100%;
margin: 0 auto;
font-family: "Inter", system-ui, sans-serif;
}
.comment-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border: 2px solid rgba(255, 255, 255, 0.1);
}
.comment-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.comment-body {
flex: 1;
min-width: 0;
}
#comment-textarea {
width: 100%;
border: 1px solid var(--cb-border);
border-radius: 12px;
padding: 0.75rem 1rem;
font-size: 0.9375rem;
font-family: inherit;
resize: none;
min-height: 44px;
max-height: 400px;
outline: none;
transition: all 0.2s ease;
overflow: hidden;
background: var(--cb-bg);
color: var(--cb-text);
backdrop-filter: blur(8px);
}
#comment-textarea::placeholder {
color: var(--cb-muted);
}
#comment-textarea:focus {
border-color: var(--cb-focus);
box-shadow: 0 0 0 3px var(--cb-focus-glow);
min-height: 100px;
background: rgba(255, 255, 255, 0.06);
}
.comment-footer {
display: none;
align-items: center;
justify-content: space-between;
margin-top: 0.75rem;
animation: fadeIn 0.3s ease;
}
#comment-textarea:focus + .comment-footer,
.comment-footer.active {
display: flex;
}
.char-limit {
font-size: 0.75rem;
color: var(--cb-muted);
}
.comment-btns {
display: flex;
gap: 0.5rem;
}
.btn-cancel,
.btn-post {
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: none;
}
.btn-cancel {
background: transparent;
color: var(--cb-muted);
border: 1px solid var(--cb-border);
}
.btn-cancel:hover {
background: var(--cb-btn-cancel-hover);
color: var(--cb-text);
}
.btn-post {
background: var(--cb-focus);
color: white;
box-shadow: 0 0 16px rgba(59, 130, 246, 0.3);
}
.btn-post:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-post:disabled {
opacity: 0.35;
cursor: not-allowed;
box-shadow: none;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}const textarea = document.getElementById("comment-textarea");
const charCountEl = document.getElementById("char-count");
const postBtn = document.getElementById("post-comment");
const cancelBtn = document.getElementById("cancel-comment");
const footer = document.getElementById("comment-footer");
textarea.addEventListener("input", () => {
// Auto-resize
textarea.style.height = "auto";
textarea.style.height = textarea.scrollHeight + "px";
// Char count
const length = textarea.value.length;
charCountEl.textContent = length;
// Enable/disable post button
postBtn.disabled = length === 0;
// Color indicator for limit
if (length >= 260) {
charCountEl.style.color = "#ef4444";
} else {
charCountEl.style.color = "";
}
});
textarea.addEventListener("focus", () => {
footer.classList.add("active");
});
cancelBtn.addEventListener("click", () => {
textarea.value = "";
textarea.style.height = "auto";
footer.classList.remove("active");
textarea.blur();
});
postBtn.addEventListener("click", () => {
if (textarea.value.trim()) {
const originalText = postBtn.textContent;
postBtn.textContent = "Posting...";
postBtn.disabled = true;
setTimeout(() => {
alert("Comment posted!");
textarea.value = "";
textarea.style.height = "auto";
postBtn.textContent = originalText;
footer.classList.remove("active");
textarea.blur();
}, 1000);
}
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Comment Box</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="comment-box-widget">
<div class="comment-avatar">
<img src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&h=100&fit=crop" alt="User" />
</div>
<div class="comment-body">
<textarea id="comment-textarea" placeholder="Write a comment..." maxlength="280"></textarea>
<div class="comment-footer" id="comment-footer">
<span class="char-limit"><span id="char-count">0</span> / 280</span>
<div class="comment-btns">
<button id="cancel-comment" class="btn-cancel">Cancel</button>
<button id="post-comment" class="btn-post" disabled>Post</button>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
type Comment = {
id: number;
user: string;
avatar: string;
color: string;
text: string;
time: string;
likes: number;
};
const INITIAL: Comment[] = [
{
id: 1,
user: "Sarah Chen",
avatar: "SC",
color: "#bc8cff",
text: "This is exactly what I was looking for! Clean API design.",
time: "2h ago",
likes: 12,
},
{
id: 2,
user: "Alex Rivera",
avatar: "AR",
color: "#58a6ff",
text: "Great implementation. One suggestion: add keyboard shortcut support.",
time: "45m ago",
likes: 7,
},
];
function CommentItem({ comment, onLike }: { comment: Comment; onLike: () => void }) {
const [liked, setLiked] = useState(false);
return (
<div className="flex gap-3">
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold text-[#0d1117] flex-shrink-0"
style={{ background: comment.color }}
>
{comment.avatar}
</div>
<div className="flex-1">
<div className="bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3">
<div className="flex items-baseline justify-between mb-1">
<span className="text-[#e6edf3] font-semibold text-sm">{comment.user}</span>
<span className="text-[#484f58] text-xs">{comment.time}</span>
</div>
<p className="text-[#8b949e] text-sm leading-relaxed">{comment.text}</p>
</div>
<div className="flex items-center gap-4 mt-1.5 ml-1">
<button
onClick={() => {
setLiked((l) => !l);
onLike();
}}
className={`text-xs transition-colors ${liked ? "text-[#ff6b6b]" : "text-[#484f58] hover:text-[#8b949e]"}`}
>
{liked ? "♥" : "♡"} {comment.likes + (liked ? 1 : 0)}
</button>
<button className="text-xs text-[#484f58] hover:text-[#8b949e] transition-colors">
Reply
</button>
</div>
</div>
</div>
);
}
export default function CommentBoxRC() {
const [comments, setComments] = useState<Comment[]>(INITIAL);
const [text, setText] = useState("");
const [submitting, setSubmitting] = useState(false);
function submit() {
if (!text.trim()) return;
setSubmitting(true);
setTimeout(() => {
setComments((prev) => [
...prev,
{
id: Date.now(),
user: "You",
avatar: "YO",
color: "#7ee787",
text: text.trim(),
time: "just now",
likes: 0,
},
]);
setText("");
setSubmitting(false);
}, 500);
}
return (
<div className="min-h-screen bg-[#0d1117] flex justify-center p-6">
<div className="w-full max-w-lg">
<h2 className="text-[#e6edf3] font-bold text-lg mb-5">{comments.length} Comments</h2>
<div className="space-y-4 mb-6">
{comments.map((c) => (
<CommentItem key={c.id} comment={c} onLike={() => {}} />
))}
</div>
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-[#7ee787] flex items-center justify-center text-xs font-bold text-[#0d1117] flex-shrink-0 mt-0.5">
YO
</div>
<div className="flex-1">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) submit();
}}
placeholder="Add a comment…"
rows={3}
className="w-full bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3 text-[#e6edf3] placeholder-[#484f58] text-sm resize-none focus:outline-none focus:border-[#58a6ff] transition-colors"
/>
<div className="flex items-center justify-between mt-2">
<span className="text-[11px] text-[#484f58]">⌘↵ to submit</span>
<button
onClick={submit}
disabled={!text.trim() || submitting}
className="px-4 py-1.5 bg-[#238636] border border-[#2ea043] text-white text-sm rounded-lg font-semibold disabled:opacity-40 hover:bg-[#2ea043] transition-colors"
>
{submitting ? "Posting…" : "Comment"}
</button>
</div>
</div>
</div>
</div>
</div>
);
}<script setup>
import { ref } from "vue";
const comments = ref([
{
id: 1,
user: "Sarah Chen",
avatar: "SC",
color: "#bc8cff",
text: "This is exactly what I was looking for! Clean API design.",
time: "2h ago",
likes: 12,
},
{
id: 2,
user: "Alex Rivera",
avatar: "AR",
color: "#58a6ff",
text: "Great implementation. One suggestion: add keyboard shortcut support.",
time: "45m ago",
likes: 7,
},
]);
const text = ref("");
const submitting = ref(false);
const likedMap = ref({});
function toggleLike(id) {
likedMap.value[id] = !likedMap.value[id];
}
function submit() {
if (!text.value.trim()) return;
submitting.value = true;
setTimeout(() => {
comments.value.push({
id: Date.now(),
user: "You",
avatar: "YO",
color: "#7ee787",
text: text.value.trim(),
time: "just now",
likes: 0,
});
text.value = "";
submitting.value = false;
}, 500);
}
function handleKey(e) {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) submit();
}
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex justify-center p-6">
<div class="w-full max-w-lg">
<h2 class="text-[#e6edf3] font-bold text-lg mb-5">{{ comments.length }} Comments</h2>
<div class="space-y-4 mb-6">
<div v-for="c in comments" :key="c.id" class="flex gap-3">
<div
class="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold text-[#0d1117] flex-shrink-0"
:style="{ background: c.color }"
>
{{ c.avatar }}
</div>
<div class="flex-1">
<div class="bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3">
<div class="flex items-baseline justify-between mb-1">
<span class="text-[#e6edf3] font-semibold text-sm">{{ c.user }}</span>
<span class="text-[#484f58] text-xs">{{ c.time }}</span>
</div>
<p class="text-[#8b949e] text-sm leading-relaxed">{{ c.text }}</p>
</div>
<div class="flex items-center gap-4 mt-1.5 ml-1">
<button
@click="toggleLike(c.id)"
class="text-xs transition-colors"
:class="likedMap[c.id] ? 'text-[#ff6b6b]' : 'text-[#484f58] hover:text-[#8b949e]'"
>
{{ likedMap[c.id] ? '\u2665' : '\u2661' }} {{ c.likes + (likedMap[c.id] ? 1 : 0) }}
</button>
<button class="text-xs text-[#484f58] hover:text-[#8b949e] transition-colors">Reply</button>
</div>
</div>
</div>
</div>
<div class="flex gap-3">
<div class="w-8 h-8 rounded-full bg-[#7ee787] flex items-center justify-center text-xs font-bold text-[#0d1117] flex-shrink-0 mt-0.5">
YO
</div>
<div class="flex-1">
<textarea
v-model="text"
@keydown="handleKey"
placeholder="Add a comment..."
rows="3"
class="w-full bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3 text-[#e6edf3] placeholder-[#484f58] text-sm resize-none focus:outline-none focus:border-[#58a6ff] transition-colors"
></textarea>
<div class="flex items-center justify-between mt-2">
<span class="text-[11px] text-[#484f58]">Cmd+Enter to submit</span>
<button
@click="submit"
:disabled="!text.trim() || submitting"
class="px-4 py-1.5 bg-[#238636] border border-[#2ea043] text-white text-sm rounded-lg font-semibold disabled:opacity-40 hover:bg-[#2ea043] transition-colors"
>
{{ submitting ? 'Posting...' : 'Comment' }}
</button>
</div>
</div>
</div>
</div>
</div>
</template><script>
let comments = [
{
id: 1,
user: "Sarah Chen",
avatar: "SC",
color: "#bc8cff",
text: "This is exactly what I was looking for! Clean API design.",
time: "2h ago",
likes: 12,
},
{
id: 2,
user: "Alex Rivera",
avatar: "AR",
color: "#58a6ff",
text: "Great implementation. One suggestion: add keyboard shortcut support.",
time: "45m ago",
likes: 7,
},
];
let text = "";
let submitting = false;
let likedMap = {};
function toggleLike(id) {
likedMap[id] = !likedMap[id];
likedMap = likedMap;
}
function submit() {
if (!text.trim()) return;
submitting = true;
setTimeout(() => {
comments = [
...comments,
{
id: Date.now(),
user: "You",
avatar: "YO",
color: "#7ee787",
text: text.trim(),
time: "just now",
likes: 0,
},
];
text = "";
submitting = false;
}, 500);
}
function handleKey(e) {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) submit();
}
</script>
<div class="min-h-screen bg-[#0d1117] flex justify-center p-6">
<div class="w-full max-w-lg">
<h2 class="text-[#e6edf3] font-bold text-lg mb-5">{comments.length} Comments</h2>
<div class="space-y-4 mb-6">
{#each comments as c (c.id)}
<div class="flex gap-3">
<div
class="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold text-[#0d1117] flex-shrink-0"
style="background: {c.color}"
>
{c.avatar}
</div>
<div class="flex-1">
<div class="bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3">
<div class="flex items-baseline justify-between mb-1">
<span class="text-[#e6edf3] font-semibold text-sm">{c.user}</span>
<span class="text-[#484f58] text-xs">{c.time}</span>
</div>
<p class="text-[#8b949e] text-sm leading-relaxed">{c.text}</p>
</div>
<div class="flex items-center gap-4 mt-1.5 ml-1">
<button
on:click={() => toggleLike(c.id)}
class="text-xs transition-colors"
class:text-[#ff6b6b]={likedMap[c.id]}
class:text-[#484f58]={!likedMap[c.id]}
>
{likedMap[c.id] ? '\u2665' : '\u2661'} {c.likes + (likedMap[c.id] ? 1 : 0)}
</button>
<button class="text-xs text-[#484f58] hover:text-[#8b949e] transition-colors">Reply</button>
</div>
</div>
</div>
{/each}
</div>
<div class="flex gap-3">
<div class="w-8 h-8 rounded-full bg-[#7ee787] flex items-center justify-center text-xs font-bold text-[#0d1117] flex-shrink-0 mt-0.5">
YO
</div>
<div class="flex-1">
<textarea
bind:value={text}
on:keydown={handleKey}
placeholder="Add a comment..."
rows="3"
class="w-full bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3 text-[#e6edf3] placeholder-[#484f58] text-sm resize-none focus:outline-none focus:border-[#58a6ff] transition-colors"
></textarea>
<div class="flex items-center justify-between mt-2">
<span class="text-[11px] text-[#484f58]">Cmd+Enter to submit</span>
<button
on:click={submit}
disabled={!text.trim() || submitting}
class="px-4 py-1.5 bg-[#238636] border border-[#2ea043] text-white text-sm rounded-lg font-semibold disabled:opacity-40 hover:bg-[#2ea043] transition-colors"
>
{submitting ? 'Posting...' : 'Comment'}
</button>
</div>
</div>
</div>
</div>
</div>Interactive Comment Box
A foundational social UI component for capturing user feedback. It features a modern, minimal design that expands upon focus, ensuring a distraction-free environment until needed.
Features
- Collapsed/Expanded states for space efficiency
- Integrated user avatar
- Real-time character limit indicator
- “Post” button with loading/disabled states
- Smooth CSS transitions for height and shadows