More Layouts Snippets
AI Chat Interface HTML CSS JS — ChatGPT-Style UI
AI Chat Interface · Layouts · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
AI Chat Interface — How to Build a ChatGPT-Style Chat UI in HTML, CSS, and JavaScript

Conversational AI interfaces have a distinctive layout that users now recognize immediately: a sidebar listing past conversations, a central message thread with alternating user and assistant bubbles, a fixed bottom input bar with an auto-resizing textarea, and suggestion chips for quick prompts. Building this pattern correctly requires careful attention to scrolling behavior, textarea height management, streaming text effects, and message bubble rendering.
This snippet builds a complete, production-quality AI chat interface in plain HTML, CSS, and vanilla JavaScript — no React, no framework, no external library. Every interaction detail matches the pattern users expect from ChatGPT, Claude, and Gemini.
Auto-Resize Textarea
The input field is a <textarea> rather than an <input type="text"> to support multi-line messages. Auto-resizing is a two-step CSS+JS technique: set height: auto to collapse the textarea to its natural height, then immediately set height: scrollHeight + 'px' to expand it to fit all content. A max-height CSS property caps the growth and enables internal scrolling for very long messages.
The resize handler fires on every input event. Setting height: auto first is the critical step — without it, the element only grows (never shrinks) because scrollHeight reads the content height including any existing expansion.
Message Bubble Architecture
Each message is a <div class="message user"> or <div class="message assistant"> containing a <div class="bubble">. User bubbles align right with an accent background. Assistant bubbles align left with a surface background, a bot icon, and inline-formatted content supporting <code>, <strong>, and <br> tags from the canned response strings.
The appendMessage(role, html) function creates this structure programmatically, appends it to the messages container, and calls scrollToBottom() — which sets messages.scrollTop = messages.scrollHeight. The scrollToBottom() call is wrapped in requestAnimationFrame to ensure the DOM has updated before the scroll happens.
Streaming Text Animation
When the assistant responds, the text is revealed character by character using setInterval. The interval fires every 18ms, appending one character to the bubble's innerHTML. The streaming creates the illusion of the AI typing in real time — the same technique used in actual AI streaming implementations, though here the full response string is known in advance.
The streaming function: creates an empty bubble, starts an interval, reads one character from the response string on each tick, appends it to the bubble, calls scrollToBottom(), and clears the interval when the full string is appended. The send button is disabled during streaming and re-enabled when complete.
Conversation Sidebar
The left sidebar lists previous conversation items using <a class="conv-item"> elements. Clicking any item sets it as active (by removing active from all items and adding it to the clicked one). In a production implementation, each item would load its message history into the main thread — the snippet keeps the click handler hookable for this extension.
The sidebar has a collapsible state toggled by a hamburger-style button. On mobile (max-width: 640px) the sidebar overlays the chat area using position: fixed and a semi-transparent backdrop. The toggleSidebar() function adds/removes the .open class which drives the transform and visibility transitions via CSS.
Suggestion Chips
On initial load (empty state), the interface shows a set of suggestion chips — common starter prompts like "Explain async/await in JavaScript" or "Write a Python function to sort a list". Clicking a chip fills the textarea with that prompt and removes the chip area. Once the first message is sent, the chip area is hidden permanently in the session. This pattern reduces friction for new users who don't know what to type.
New Chat and Conversation History
The New Chat button (+ icon in the sidebar header) calls newChat() — clears the message thread, restores the suggestion chips, resets the input, focuses the textarea, and creates a new conversation entry in the sidebar list. The conversation entry uses the first message text (truncated to 30 characters) as its label, simulating the pattern of AI apps naming conversations after their first query.
Input Bar Layout
The input bar uses a display: flex row with the textarea taking flex: 1 and the send button fixed-width. The Enter key submits (via keydown event checking e.key === 'Enter' && !e.shiftKey), while Shift+Enter inserts a newline — the same keyboard behavior as ChatGPT and Claude's web interface. e.preventDefault() on plain Enter prevents the default form newline.
Styling the Code Blocks
The assistant responses include HTML with inline <code> tags. The CSS .bubble code styles these with a monospace font, a subtle background tint, and rounded corners — matching the pattern of AI chat interfaces that render markdown-style code inline without a full syntax highlighter.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to trace the auto-grow and scroll logic by hand — paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the textarea's height is reset to auto before reading scrollHeight on every input event, and what would break if that reset were skipped. The same assistant is useful for optimizing it — asking whether appendMessage's approach of building a fresh DOM subtree per message is fast enough for a conversation with hundreds of messages, or whether older messages should be virtualized out of the DOM. It's just as good for extending the interface: ask it to replace the setTimeout-based canned responses with a real streaming fetch call to an LLM API using ReadableStream, add markdown/code-block rendering to assistant bubbles, or persist conversations to localStorage so they survive a page reload. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a ChatGPT-style "AI chat interface" in plain HTML, CSS, and JavaScript — no framework, no libraries, using a sidebar plus main chat panel layout.
Requirements:
- A fixed-width dark sidebar containing a "new chat" button, a list of clickable past-conversation entries (one marked active at a time), and a user row at the bottom; a flexible main panel containing a header with a clickable model-name selector, a scrollable message thread, an empty-state row of clickable suggestion chips, and a bottom input bar.
- The message input must be a textarea (not a single-line input) that automatically grows taller as the user types: on every input event, first reset its height to auto, then set it to its scrollHeight in pixels, capped at a maximum height beyond which it scrolls internally instead of growing further.
- Enter must send the message and Shift+Enter must insert a newline: implement this by checking e.key and e.shiftKey in a keydown handler and calling preventDefault only for the plain-Enter case.
- Sending a message must: append a right-aligned user bubble, clear and shrink the input, disable the send button, show a left-aligned "typing" indicator (three bouncing dots with staggered animation-delay values) in place of the assistant's response, and after a delay remove the typing indicator and append the assistant's reply bubble (built from a small rotating array of canned HTML responses for this demo, structured so a real API call could replace it later).
- Every new message appended to the thread must trigger the message container to scroll to its bottom automatically.
- Clicking a suggestion chip should fill the input with that chip's preset prompt text and focus the input, without sending it automatically. Clicking "new chat" should clear the thread, restore the suggestion chips, and reset the input.Step by step
How to Use
- 1Start a conversationType a message in the input bar and press Enter (or Shift+Enter for a new line). The user bubble appears on the right, and after a brief delay the assistant responds with a streaming text animation.
- 2Use suggestion chipsOn the empty state, click any suggestion chip to pre-fill the input with a common prompt. The chips disappear after the first message is sent.
- 3Toggle the sidebarClick the hamburger icon to collapse or expand the conversation history sidebar. On mobile, the sidebar overlays the chat with a backdrop.
- 4Start a new chatClick the + button in the sidebar header to clear the message thread and start fresh. The previous conversation appears as a new entry in the sidebar list.
- 5Connect a real AI APIIn the sendMessage() function, replace the canned response with a fetch() call to OpenAI, Anthropic, or any streaming AI endpoint. Replace the character-by-character interval with a ReadableStream reader for true server-sent streaming.
- 6Add markdown renderingReplace the inner HTML string assignment with a markdown parser (marked.js, markdown-it) for full bold, code block, and list rendering. The bubble structure and streaming loop stay the same.
Real-world uses
Common Use Cases
<code> rendering and streaming animation are already wired. Connect to a code-specialized model and add a code editor panel alongside the chat thread.Got questions?
Frequently Asked Questions
In sendMessage(), replace the canned response setTimeout with: const res = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Authorization": "Bearer " + API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ model: "gpt-4o", messages: conversationHistory, stream: true }) }). Read the response as a ReadableStream and decode each SSE chunk to get the delta text, appending it to the assistant bubble in real time.
The input handler does: textarea.style.height = "auto"; then immediately textarea.style.height = textarea.scrollHeight + "px". Setting height to auto first collapses the element so scrollHeight reflects the actual content height. Without the collapse step, scrollHeight always reads the previously set height and the textarea never shrinks. A CSS max-height caps growth and enables internal scrolling for very long inputs.
Add the marked.js CDN script, then in the appendMessage function change the bubble innerHTML assignment to: bubble.innerHTML = marked.parse(responseText). This renders bold, code, ``code blocks``, and - list items correctly. For the streaming animation, accumulate the full response in a string variable and call marked.parse() on the complete string after streaming ends, replacing the streaming innerHTML with the rendered markdown.
Serialize the messages array to localStorage on each message: localStorage.setItem("chat-history", JSON.stringify(messages)). On page load, read it back and re-render each message. For the sidebar conversation list, store an array of conversation objects { id, title, messages } in localStorage keyed by a generated UUID. This matches the persistence pattern of real AI chat apps.
Add a keydown listener to the textarea: textarea.addEventListener("keydown", e => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); } }). The !e.shiftKey condition means plain Enter sends, while Shift+Enter falls through to the default behavior (newline insertion). Always call e.preventDefault() on plain Enter to prevent the textarea from adding a newline before submitting.
Yes. The JSX, Vue, Angular, and Tailwind export buttons convert it automatically. In React, keep the message array in useState, replace the typing simulation with your streaming API call, and scroll the container in a useEffect that runs whenever messages change.