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.
- Create the container as a flex column with a set height.
- Add the header at a fixed height, with the close or minimize control.
- Add the message list with
flex: 1,min-height: 0andoverflow-y: auto. - Add the input bar, with a text field that grows and a send button that keeps its width.
- 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.
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.
| Method | JavaScript | DOM order | Where it fails |
|---|---|---|---|
| column-reverse | No | Reversed | Screen reader order differs from visual order |
| overflow-anchor sentinel | No | Chronological | No support in Safari 26 and earlier |
| scroll-snap end sentinel | No | Chronological | Re-snap animation is up to the browser |
| scrollTop or scrollIntoView | Yes | Chronological | Can 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,bottomandrightoffsets, and az-indexabove host content. - Show a round launcher button while the window is closed.
- Toggle an
.is-openclass that fades and slides the window withopacityandtransform, then setsvisibility: hiddenon 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.
| Unit | Measures | In a chat box |
|---|---|---|
| vh | Large viewport, toolbars retracted | Input bar can sit behind the toolbar |
| svh | Small viewport, toolbars shown | Stable height, never clipped |
| dvh | Whatever is visible right now | Tracks 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-livevalue ofrole="log"is polite (MDN). - Its implicit
aria-atomicvalue 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.
| Need | CSS alone | What it takes |
|---|---|---|
| Layout, bubbles, theming | Yes | Stylesheet only |
| Sending and receiving | No | Script plus server |
| Real-time delivery | No | WebSocket connection |
| Long histories | Partly | content-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.
- What is Backend in Web Development? - September 13, 2026
- What is Frontend Development? - September 11, 2026
- How to Make a Button in Figma: Design Best Practices - September 10, 2026


