AI Chat Interface HTML CSS JS — ChatGPT-Style UI

AI Chat Interface · Layouts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Auto-resize textarea: height:auto → scrollHeight trick, max-height cap for internal scroll, fires on every input event
Streaming text: setInterval 18ms character-by-character reveal — same UX as real AI streaming responses
Message bubbles: user (right, accent bg) and assistant (left, surface bg) with bot icon and inline HTML rendering
Suggestion chips: empty-state prompts, click-to-fill, hidden permanently after first message sent
Sidebar: conversation history list, active item tracking, collapsible with CSS transform transition
Mobile sidebar: position:fixed overlay with backdrop, .open class toggle via toggleSidebar()
New Chat: clears thread, restores chips, creates sidebar entry with first-message-truncated label
Enter to send, Shift+Enter for newline — preventDefault on plain Enter prevents default form newline
scrollToBottom: messages.scrollTop = messages.scrollHeight, wrapped in requestAnimationFrame for DOM update timing

About this UI Snippet

AI Chat Interface — How to Build a ChatGPT-Style Chat UI in HTML, CSS, and JavaScript

Screenshot of the AI Chat Interface snippet rendered live

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:

text
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

  1. 1
    Start 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.
  2. 2
    Use 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.
  3. 3
    Toggle the sidebarClick the hamburger icon to collapse or expand the conversation history sidebar. On mobile, the sidebar overlays the chat with a backdrop.
  4. 4
    Start 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.
  5. 5
    Connect 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.
  6. 6
    Add 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

AI Assistant & Chatbot UI Template
Use as the front-end shell for any AI assistant product — connect to OpenAI, Anthropic Claude, Google Gemini, or any custom LLM API. The streaming simulation matches the UX of real streaming responses, so swapping in a real ReadableStream reader requires only changing the response handler.
Customer Support & Help Chat Widget
Embed as a support chat interface connected to a rule-based chatbot or live agent backend. The sidebar conversation history, suggestion chips for common questions, and mobile overlay sidebar match the patterns users expect from modern support chat tools.
Developer Tool Copilot Interface
Build a coding assistant UI that accepts code questions and returns syntax-highlighted answers. The inline <code> rendering and streaming animation are already wired. Connect to a code-specialized model and add a code editor panel alongside the chat thread.
Product Prototype & UX Demo
Use the canned responses and suggestion chips to demo an AI-powered feature to stakeholders or user research participants without building a real backend. The streaming animation and familiar ChatGPT-style layout make demos feel polished and production-ready.
Onboarding Bot & Interactive Tutorial
Replace canned responses with a scripted conversation tree that guides new users through product features step by step. Each step's response advances the tutorial. Pair with an onboarding tour that highlights UI elements in sync with the chat messages.
Language Learning & Conversation Practice
Build a language learning conversation partner that responds in the target language. The suggestion chips can scaffold early learners with pre-written prompts. The message history sidebar lets learners review past practice sessions. Connect to any multilingual model API.

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.