Most of a chat box is a flex column with rounded corners. Bubbles, a header, a scrolling message list and an input bar at the bottom, all styled with plain CSS and no chat library.

What CSS can’t do is the chatting. Sending, receiving and storing messages still takes a script and a server.

It does handle one behavior on its own, which is staying on the newest message. The W3C CSS Scroll Snap Level 1 specification (2021) does it with an empty ::after element set to scroll-snap-align: end.

What Are the Parts of a CSS Chat Box?

See the Pen
Chat UI Responsive
by abadu (@abadu)
on CodePen.

The whole thing hangs off one container div, set to display: flex and flex-direction: column, with a fixed or capped height. The header at the top holds the title and a status line, and usually the close or minimize button. The message list takes whatever height is left through flex: 1 and scrolls with overflow-y: auto. At the bottom sits the input bar, which is a form with a text field and a send button.

The flex column handles the sizing. Header and input bar keep their natural height, and the message list takes everything else.

One mistake shows up in a lot of first builds. Flex items default to min-height: auto, so a message list without min-height: 0 grows with its content instead of scrolling.

As interface components go, this one is forgiving. Many popular templates reuse the same class names, chat-box, chat-box-header, chat-box-body and chat-box-footer.

Keep the markup semantic. A form for the input bar and a button for send give you Enter-to-submit and keyboard focus behavior for free, with no extra HTML attributes.

For a first pass, I’d build it in this order.

  1. Create the container as a flex column with a set height.
  2. Add the header at a fixed height, with the close or minimize control.
  3. Add the message list with flex: 1, min-height: 0 and overflow-y: auto.
  4. Add the input bar, with a text field that grows and a send button that keeps its width.
  5. Paste in enough dummy messages to overflow the list, so scroll problems show up before styling starts.

Styling Message Bubbles and Tails

See the Pen
Chat Bubble
by RaKesh Mandal (@rKalways)
on CodePen.

Padding, border-radius, max-width and a background with enough contrast against its text are what make a div read as a bubble. Tails, timestamps and avatars sit on top of that base.

What is shaping UX design today?

Uncover the newest UX design statistics: user behavior, design trends, ROI data, and insights driving better digital experiences.

Check Them Out →

Bubble Shape and Spacing

Roughly 8px by 12px of padding keeps short replies compact. A large border radius gives the pill shape, and squaring one corner points the bubble toward the speaker.

Cap the width somewhere between 70% and 80% of the list so long messages don’t span the whole box. Add overflow-wrap: anywhere too, because a long URL will otherwise push the bubble past its edge.

Timestamps go on a small second line inside the bubble or beside it. Avatars live in the row wrapper with align-items: flex-end, so they line up with the last line of the message.

Tails With Pseudo-Elements

The classic route is a ::before or ::after box, absolutely positioned at the bubble’s corner and drawn as a border triangle or a rotated square. It’s the speech bubble technique most tutorials teach, and the easiest one to read.

Smashing Magazine’s 2024 two-part guide goes a different way and builds the tail on a single element, with CSS variables controlling the tail’s size, shape and position. The tail is cut into the main shape with clip-path or a mask, so one background, even a gradient, fills bubble and tail together.

Each approach breaks somewhere. A border-triangle tail cannot match a gradient background, and a clip-path cuts off anything painted outside the shape, including box-shadow.

Borders are the hard case for the clip-path method. Smashing’s second part adds a pseudo-element that traces the shape at a smaller size, because a plain border cannot follow the clipped outline.

Typing Indicator Animation

The typing indicator is three dots in a received bubble, each running the same animation at a different delay.

Define one keyframes rule that lifts a dot a few pixels and drops it back, then stagger animation-delay across the spans. Give the spans border-radius: 50% and set animation-iteration-count: infinite so the loop never stops.

How to Align Sent and Received Messages

See the Pen
WhatsApp in Pure CSS and JS
by Zeno Rocha (@zenorocha)
on CodePen.

Set align-self: flex-end on sent messages and align-self: flex-start on received ones, inside a message list that is a flex column. Auto margins give the same result, because margin-inline-start: auto absorbs the free space and pushes a sent bubble to the right.

Pick one method and stay with it. Mixing align-self on some bubbles and auto margins on others is how alignment bugs start.

Sent bubbles get the end alignment, an accent background, light text and a squared bottom-right corner. Received ones go the other way: start alignment, a neutral background, dark text, squared bottom-left corner.

Consecutive messages from one sender read better as a group. The adjacent sibling selector (.sent + .sent) tightens the gap, and a modifier class such as .no-tail removes the tail from every bubble except the last one in the group.

Logical properties make the layout follow the writing direction. Use margin-inline-start and border-end-end-radius instead of left and right values, and Arabic or Hebrew interfaces flip without a second stylesheet.

Keeping the Newest Message in View

Pinning a chat list to its newest message can be done several ways, and each one fails somewhere different. All of them start from the same base: a bounded height, overflow-y: auto and, for animated jumps, scroll-behavior: smooth.

MethodJavaScriptDOM orderWhere it fails
column-reverseNoReversedScreen reader order differs from visual order
overflow-anchor sentinelNoChronologicalNo support in Safari 26 and earlier
scroll-snap end sentinelNoChronologicalRe-snap animation is up to the browser
scrollTop or scrollIntoViewYesChronologicalCan run late, mid-scroll

The sources split on the first row. MDN says the reverse values of flex-direction create a gap between visual and DOM order, and the flexbox specification tells authors not to use them in place of correct source order.

CSS-only chat tutorials recommend it anyway. Kitty Giraudel’s 2024 write-up on bottom-anchored scrolling does, while flagging the same screen reader problem.

Chrome 137 added the reading-flow property, which lets focus order follow the visual order inside flex containers (Chrome for Developers). Rachel Andrew’s talk on it adds a warning: do not use it to avoid fixing the HTML order.

The overflow-anchor method puts a pixel-high sentinel at the end of the list with overflow-anchor: auto and opts the rest of the list out with none. Chrome 56 (2017) and Firefox 66 (2019) support it. Safari 27, released in September 2026, added scroll anchoring and the overflow-anchor property, and web.dev now lists the feature as Baseline newly available. Safari 26 and earlier have no support, so visitors on older iPhones and Macs still need a fallback.

The scroll-snap method comes from the W3C spec itself. The CSS Scroll Snap Level 1 candidate recommendation (March 2021) includes a log console example, with a proximity scroll-snap-type on the container and an empty ::after element set to scroll-snap-align: end.

New messages stay in view while the user sits near the bottom, and nothing happens once they scroll up to read history. The spec requires the re-snap but leaves the animation of a re-snap to the same element to the browser.

Plain JavaScript is the most predictable fallback. Giraudel notes the catch: a script can fail or run late, after the user has already started scrolling.

Skip column-reverse for production. The shortest code is not worth a reading order that screen reader users cannot trust.

For a prototype or demo it’s fine, and it is the least code. A production widget should keep chronological DOM order, use the scroll-snap sentinel, and call scrollIntoView on the last message as a fallback. And if Safari 26 or earlier is in your audience, don’t rely on overflow-anchor alone. Safari 27 and later support it.

How to Build a Floating CSS Chat Box in the Bottom Corner

See the Pen
chatbot template
by Burlington Landbrown (@Burlington_Landbrown)
on CodePen.

Fixed positioning does the pinning, and one class on the container does the opening and closing. MDN’s position reference (revised July 2026) adds that a fixed element always creates its own stacking context.

Intercom’s Messenger shows the pattern at scale. It sits at the bottom right by default, and its developer documentation exposes alignment, horizontal_padding and vertical_padding settings to move the launcher.

On mobile web those offsets stop mattering. Intercom’s team says on its community forum that the launcher stays at the bottom right and the three settings are ignored, so treat the phone layout as its own case.

  • Pin the container with position: fixed, bottom and right offsets, and a z-index above host content.
  • Show a round launcher button while the window is closed.
  • Toggle an .is-open class that fades and slides the window with opacity and transform, then sets visibility: hidden on close so keyboard focus cannot reach an invisible window.
  • Let the minimize button remove the class and return focus to the launcher.
  • The unread badge is positioned absolutely on the launcher, which needs position: relative.

The widget also stops being fixed if an ancestor has transform, perspective or filter set to anything but none. That ancestor becomes the containing block in place of the viewport, and the corner offsets measure from it.

MDN documents the same exception for will-change: transform. Mount the chat markup as a direct child of body to avoid it, since a page-transition wrapper that applies a transform is one way it happens.

Making a CSS Chat Box Work on Mobile Screens

See the Pen
Draggable Chatbox
by Andy Tran (@andytran)
on CodePen.

On a phone the chat box should fill the screen. Size it with dvh instead of vh, tell Chrome how to treat the on-screen keyboard, and pad the input bar for the safe area.

Start from the phone layout. The container is position: fixed with inset: 0, so it fills the screen, and a min-width media query then shrinks it into the corner widget on larger screens.

That order is the core of a mobile-first approach, and it keeps the desktop rules short because they only override a handful of values.

UnitMeasuresIn a chat box
vhLarge viewport, toolbars retractedInput bar can sit behind the toolbar
svhSmall viewport, toolbars shownStable height, never clipped
dvhWhatever is visible right nowTracks the toolbar, reflows as it moves

MDN says the default vh unit currently equals its large viewport counterpart, lvh. web.dev adds the catch for dvh: the dynamic values do not update at 60fps, so do not count on them for smooth animation.

Chrome 108 changed the keyboard behavior. Chrome on Android stopped resizing the layout viewport when the on-screen keyboard opens (Chrome for Developers).

The viewport meta tag now takes an interactive-widget key, and its values are resizes-visual (the default), resizes-content and overlays-content. Chat boxes usually want resizes-content, which shrinks the layout viewport with the keyboard, so a box sized with dvh follows it.

The full tag reads <meta name="viewport" content="width=device-width, initial-scale=1, interactive-widget=resizes-content">. Chrome documents the key, so check the engines you target before relying on it.

Notched phones add one more edge. Add viewport-fit=cover to the same meta tag, then pad the input bar with env(safe-area-inset-bottom) so the home indicator does not sit on top of the send button.

MDN notes the inset values are zero on rectangular displays, so the padding costs nothing on a laptop.

Make the send button as tall as the input field so a thumb hits it on the first try. Then test with the keyboard open, because a static screenshot never shows that state.

Dark Mode and Theming With Custom Properties

Store every color as a custom property on :root, then redefine the values inside @media (prefers-color-scheme: dark). The bubbles, header and input bar all retheme from one block of CSS.

The tokens worth defining are surface and border, the sent and received bubble backgrounds with their text colors, the input field and its placeholder, and the focus ring.

Point the tail, the typing dots and the timestamp at the same tokens. A tail that reads var(--bubble-received) flips with its bubble and never drifts out of sync.

Support is settled. MDN lists prefers-color-scheme as Baseline widely available, across browsers since January 2020.

The media query method keeps the theme in one block, though every overridden token gets written twice, once per scheme. light-dark() is leaner, with one declaration per color, such as light-dark(#333b3c, #efefec), and color-scheme: light dark set on :root (MDN).

An embedded widget can land on a host page with a different theme. Setting color-scheme to light or dark on the chat box forces its scheme, which MDN documents for page sections.

For a manual toggle, key the same overrides to a data-theme attribute on the container and let the media query set only the default.

Check both token sets for color contrast. A bubble pair that passes in light mode can fail after the tokens flip, especially white text on a mid-tone sent bubble.

Is a CSS Chat Box Accessible by Default?

No. A plain div of bubbles stays silent when a new message arrives, and an unlabeled input gives a screen reader nothing to announce.

Start with the message list. Give it role="log", an ARIA role, and an accessible name through aria-label or aria-labelledby. MDN documents the role’s defaults as aria-live="polite" and aria-atomic="false", so only new messages are read, and only when the user is idle.

W3C’s ARIA23 technique uses a chat history as its working example, with the log named by a heading. Switching to aria-live="assertive" interrupts the reader, so keep it for urgent system notices.

The input needs a label, visible or through aria-label, and an icon-only send button needs a name too. The rules for any accessible form apply, because the input bar is one.

Style :focus-visible on the input and the send button, and move focus into the input when the widget opens.

Motion deserves a guard. Wrap the typing bounce, smooth scrolling and slide transitions in @media (prefers-reduced-motion: no-preference). MDN frames the setting as removing motion that is not needed, so a short opacity fade can stay while the bounce and the slide go.

These are the values I’d test against.

  • 4.5:1 is the minimum contrast for text against its background, criterion 1.4.3 (W3C).
  • For the visual information that identifies the input, the send button and their focus states, the minimum is 3:1, criterion 1.4.11 (W3C).
  • Pointer targets need to be at least 24 by 24 CSS pixels, criterion 2.5.8, new in WCAG 2.2 (W3C).
  • The implicit aria-live value of role="log" is polite (MDN).
  • Its implicit aria-atomic value is false (MDN).

W3C treats both contrast ratios as thresholds with no rounding, so a computed 4.499:1 fails the 4.5:1 requirement.

Where a CSS-Only Chat Box Stops Working

CSS draws the chat box and nothing else. It cannot send, receive or store a message. Those jobs belong to a script and a server.

NeedCSS aloneWhat it takes
Layout, bubbles, themingYesStylesheet only
Sending and receivingNoScript plus server
Real-time deliveryNoWebSocket connection
Long historiesPartlycontent-visibility plus script paging

On the frontend that means a submit handler, DOM updates and a scroll call after each append. Static demos fake all of it.

The backend owns accounts, storage and delivery. Without one, messages live only in the tab that typed them.

The usual transport is the WebSocket API, which MDN describes as a two-way session between browser and server with no polling. MDN also warns that an open connection may keep the page out of the back/forward cache, so close it when the conversation ends.

Long histories are the failure CSS can only partly fix. GitHub’s Primer React added content-visibility: auto to its ActionList items so the browser skips layout and paint for off-screen rows, an idea a message list can borrow.

There are catches. DebugBear’s guide says skipped content needs contain-intrinsic-size so the browser can reserve its space, and content-visibility: auto applies paint containment, which can clip a tail or shadow hanging outside a row.

Primer’s own change excludes items with an active indicator line for that same reason.

Loading older messages above the current position brings back the scroll problem. MDN describes scroll anchoring as the behavior that reduces unintended scrolling when content changes above the scrollport.

Safari 26 and earlier lack the overflow-anchor property, so restore scrollTop with a script after each prepend for those visitors. Safari 27 added scroll anchoring, so newer versions behave like Chrome and Firefox.

Treat the CSS-only version as a design prototype. Real conversations need the script and the server.

CSS Chat Box Examples To Check Out

Game of Thrones Chat Fest

See the Pen
Game of Thrones chatbox
by ajay sunarthi (@ajaysunarthi)
on CodePen.

Pure CSS Gab Corner

See the Pen
CSS chat
by Bishonenlover (@bishonenlover)
on CodePen.

Bare Minimal Chatroom

See the Pen
Minimal Chatbox
by Xava (@atagulalan)
on CodePen.

Rowin’s Chatterbox

See the Pen
Chatbox
by Rowin (@ruizenr1)
on CodePen.

Slack but Sassy

See the Pen
Slack Sass
by Brandon (@brandonfujii)
on CodePen.

Direct Messages, CSS Style

See the Pen
Daily UI #013 – Direct Messaging
by Julie Park (@juliepark)
on CodePen.

WhatsApp Me, Maybe?

See the Pen
Multiple Whatsapp Chat Widget – By Dunia Blanter
by Rhinokage Rio (@Idblanter)
on CodePen.

All Chat, No Real Talk

See the Pen
A Fake Chatbox
by k (@keper11)
on CodePen.

Keeping Chats Simple

See the Pen
100DaysCSS-1
by iamJoey (@YousifW)
on CodePen.

Chats with a Groove

See the Pen
Animated chat bar interaction
by Kyle Wetton (@kylewetton)
on CodePen.

The Kluge Chat Experience

See the Pen
Chatbox
by Theodore Kluge (@tkluge)
on CodePen.

Land and Chat

See the Pen
Animated Chat App Landing Page
by Rahul Sah (@rahulsahofficial)
on CodePen.

Swank it up with Chatbot V2

See the Pen
Swanky Chatbox V2
by Jack Thomson (@Jackthomsonn)
on CodePen.

Robin’s Chit-Chat Cube

See the Pen
[CSS] Chatbox
by Robin Llopis (@robinllopis)
on CodePen.

Moonlit Messages by Aysenur

See the Pen
Messaging App UI with Dark Mode
by Aysenur Turk (@TurkAysenur)
on CodePen.

Debashis’ Doorway to Chats

See the Pen
Live Chat
by Debashis Barman (@debashisbarman)
on CodePen.

Shape-Shifting Convos with Amit

See the Pen
Anime JS Shape Morphing chatbox
by Amit Singh (@amitasaurus)
on CodePen.

Artistic Artem’s Chat Window

See the Pen
Chat UI modal window
by Artem Panarin (@panart)
on CodePen.

Navvy: The Friendly Guide

See the Pen
Navvy the navigational ChatBot!
by Mees (@meesrutten)
on CodePen.

Gaming Vibes with Marcos

See the Pen
Twitch Chatbox Theme
by Marcos Vinícius Koga e Silva (@KogaSilverDragon)
on CodePen.

Virgil’s Chat Aesthetic

See the Pen
Untitled
by Virgil Pana (@virgilpana)
on CodePen.

Haja’s Feature-Packed Powwow

See the Pen
Mock Chat Interaction
by Haja Randriakoto (@haja-ran)
on CodePen.

Yan’s Classic Chat Corner

See the Pen
ChatBox
by yanzhang-sheridan (@yanzhang-sheridan)
on CodePen.

Chatbot with a Cherry on Top

See the Pen
🤖 Chatbot Assistant Prototype
by Guimauve (@Guimauve01)
on CodePen.

Rami’s Chat with a Personal Touch

See the Pen
Responsive Chat Widget
by Rami Lulu (@ramilulu)
on CodePen.

Jessica’s Dropdown Delight

See the Pen
Dropdown chatbox
by Jessica Pekanto (@jpekanto)
on CodePen.

Ride the Emotional Wave with Hyperplexed

See the Pen
Emotional Chatbot
by Hyperplexed (@Hyperplexed)
on CodePen.

Shiva’s Colorful Conversations

See the Pen
Chat UI
by Shiva Pandey (@shivapandey)
on CodePen.

Chat but Make it Stealthy

See the Pen
Show Hide Chat Box With Minimize
by Adhy Suryadi (@kompiajaib)
on CodePen.

Ionel’s Hipster Hangout

See the Pen
HTML/CSS implementation of HipChat redesign
by Ionel Cucu (@CucuIonel)
on CodePen.

Kristina’s Bubble Banter

See the Pen
Flex chat bubbles | Responsive chat bubbles
by Kristina (@kristinak)
on CodePen.

Mamun’s Snazzy Space

See the Pen
Stylish chat window design
by Mamun Khandaker (@kh-mamun)
on CodePen.

Thomas’ Modern Messaging

See the Pen
Material Messaging App Concept
by Thomas d’Aubenton (@ThomasDaubenton)
on CodePen.

Alissa’s Animated Awesomeness

See the Pen
Pure CSS Chat Bubble Animations [WIP]
by Alissa (@alissarenz)
on CodePen.

CSS Chat Box FAQ

Can I build a chat box with Bootstrap or Tailwind CSS?

Yes. Tailwind utilities such as flex-col, rounded-2xl and self-end cover the column layout, bubble radius and sender alignment, and Bootstrap offers d-flex, flex-column and align-self-end.

Tails and typing dots still need a few lines of custom CSS.

Should a chat layout use Flexbox or Grid?

See the Pen
JS responsive chatbox flexbox and localStorage
by jaguar (@jaguar66)
on CodePen.

Either works, and Flexbox with a column direction is the simpler default.

A grid with rows of auto minmax(0, 1fr) auto pins the header and input bar just as well, and minmax(0, 1fr) keeps the message list from outgrowing its container.

How do I stop the page behind the chat box from scrolling?

Set overscroll-behavior: contain on the message list. It stops scroll chaining when the list hits its top or bottom edge.

On a full-screen phone layout, also lock the body while the box is open, for example with overflow: hidden.

Does a CSS chat box work in React or Vue?

Yes. CSS does not depend on a framework, so the same stylesheet works when a component renders the bubbles from a message array.

The framework changes how new messages get appended, not how they look.

How do I animate a new message sliding in?

See the Pen
Daily UI #013 | Direct Messaging
by Mubanga (@mubangadv)
on CodePen.

Write a short @keyframes rule that moves the bubble from a small translateY offset and zero opacity to its resting position, then apply it to the message class so every appended bubble plays it once.

Keep it brief, and switch it off under prefers-reduced-motion: reduce.

Why does the chat input zoom the page on an iPhone?

Mobile Safari zooms into a focused input whose font size is below 16px.

Set the input to 16px or larger and the zoom stops. Disabling user scaling instead would hurt accessibility.

Choosing a CSS Chat Box Build for Your Project

A CSS chat box suits a static template, a widget on an existing site, or the front layer of a full messaging app, and what decides it is where the messages come from.

Settle the message source first, whether static, polled or WebSocket. Markup structure comes next, meaning the log role and the scroll method. Polish such as theming, motion and bubble tails can wait until last.

A change of message source rewrites the markup and scripts around it, while a theme change touches only custom properties.

A custom build avoids a third-party widget script, but it takes on the accessibility work that a maintained library would otherwise carry.

Position verified October 14, 2026. Safari 27 (September 2026) now supports overflow-anchor, so the script-free sentinel method works in every current major browser. Keep a fallback only for visitors on Safari 26 and earlier.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)