Bare
No panel, no shadow. It inherits your page.
Live preview · real tokens, not a screenshot
When to pick it
The least opinionated design in the gallery. Bare drops the panel entirely and renders straight onto your background, taking its type from whatever is around it. Use it when the chat belongs inside a page — on a contact page, at the end of a help article — rather than floating above one.
How it is built
The widget surface is transparent and the only structure is a single top hairline. Because it is still inside a Shadow DOM, it cannot actually inherit your CSS — font and colour are passed in as tokens and matched deliberately.
Accessibility
Contrast depends on your page, which is the trade-off. The config validates your chosen text colour against your chosen background and refuses to save below 4.5:1.
Every design in this gallery states that it is an AI assistant at the start of the conversation and honours prefers-reduced-motion. Neither is configurable, because both are obligations rather than preferences.
Good fit for
- Help centres
- Contact pages
- Inline support
- Blogs
Source code · MIT
Copy it and go.
Self-contained: no build step, no dependency, no ChatBrace account. Point send() at your own endpoint, or at ours. MIT licensed.
<!-- Bare — chat widget. MIT licensed. -->
<div id="cb" data-open="false">
<button id="cb-launch" aria-expanded="false" aria-controls="cb-panel">Chat</button>
<div id="cb-panel" role="dialog" aria-modal="false" aria-label="Chat with Northwind Supply" hidden>
<header id="cb-head">
<span id="cb-avatar" aria-hidden="true">N</span>
<span>
<strong>Northwind Supply</strong>
<!-- Required: people must be told they are talking to an AI
before the conversation starts. Do not remove this line. -->
<small>AI assistant</small>
</span>
<button id="cb-close" aria-label="Close chat">×</button>
</header>
<div id="cb-log" role="log" aria-live="polite" aria-atomic="false"></div>
<form id="cb-form">
<label class="cb-sr" for="cb-input">Your message</label>
<input id="cb-input" autocomplete="off" placeholder="Ask a question" maxlength="500" />
<button type="submit" aria-label="Send message">↑</button>
</form>
</div>
</div>
<style>
#cb { --bg:transparent; --surface:transparent; --border:#e5e5e5;
--text:#18181b; --muted:#71717a; --accent:#18181b;
--on-accent:#ffffff; --bot-bg:transparent; --bot-text:#18181b;
--radius:0px; --bubble:8px;
--ease:cubic-bezier(.16,1,.3,1);
position:fixed; inset-block-end:24px; inset-inline-end:24px; z-index:2147483000;
font-family:ui-sans-serif, system-ui, -apple-system, sans-serif; }
.cb-sr { position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); }
#cb-launch { all:unset; box-sizing:border-box; cursor:pointer; display:grid; place-items:center;
min-width:52px; height:52px; padding:0 18px; border-radius:999px;
background:var(--accent); color:var(--on-accent); font-size:14px; font-weight:600;
box-shadow:none; transition:transform .2s var(--ease); }
#cb-launch:hover { transform:translateY(-2px); }
#cb-launch:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
#cb-panel { position:absolute; inset-block-end:64px; inset-inline-end:0;
width:min(380px, calc(100vw - 32px)); background:var(--bg);
border:1px solid var(--border); border-radius:var(--radius);
box-shadow:none; overflow:hidden;
animation:cb-in .28s var(--ease) both; }
#cb-head { display:flex; align-items:center; gap:10px; padding:11px 14px;
background:var(--surface); border-bottom:1px solid var(--border); }
#cb-head strong { display:block; font-size:13px; color:var(--text); }
#cb-head small { display:block; font-size:11px; color:var(--muted); }
#cb-avatar { width:26px; height:26px; flex:0 0 auto; display:grid; place-items:center;
border-radius:6px;
background:var(--accent); color:var(--on-accent); font-size:11px; font-weight:600; }
#cb-close { all:unset; cursor:pointer; margin-inline-start:auto; padding:4px 8px;
color:var(--muted); font-size:18px; line-height:1; }
#cb-close:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
#cb-log { display:flex; flex-direction:column; gap:8px; padding:14px;
height:300px; overflow-y:auto; }
.cb-msg { max-width:88%; padding:8px 11px; font-size:13px; line-height:1.5;
border-radius:var(--bubble); animation:cb-msg .3s var(--ease) both; }
.cb-user { align-self:flex-end; background:var(--accent); color:var(--on-accent); }
.cb-bot { align-self:flex-start; background:var(--bot-bg); color:var(--bot-text);
border:1px solid var(--border); }
#cb-form { display:flex; align-items:center; gap:8px; padding:10px 14px;
background:var(--surface); border-top:1px solid var(--border); }
#cb-input { flex:1; min-width:0; background:none; border:none; outline:none;
color:var(--text); font:inherit; font-size:13px; }
#cb-input::placeholder { color:var(--muted); }
#cb-form button { all:unset; cursor:pointer; width:28px; height:28px; display:grid;
place-items:center; border-radius:6px;
background:var(--accent); color:var(--on-accent); font-size:14px; }
#cb-form button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
@keyframes cb-in { from{opacity:0; transform:translateY(8px) scale(.98)} to{opacity:1; transform:none} }
@keyframes cb-msg { from{opacity:0; transform:translateY(6px)} to{opacity:1; transform:none} }
/* Vestibular safety: motion is removed, not shortened. */
@media (prefers-reduced-motion: reduce) {
#cb-panel, .cb-msg { animation:none; }
#cb-launch { transition:none; }
#cb-launch:hover { transform:none; }
}
</style>
<script>
(function () {
var root = document.getElementById('cb');
var panel = document.getElementById('cb-panel');
var launch = document.getElementById('cb-launch');
var close = document.getElementById('cb-close');
var form = document.getElementById('cb-form');
var input = document.getElementById('cb-input');
var log = document.getElementById('cb-log');
var conversationId = null;
function toggle(open) {
panel.hidden = !open;
root.dataset.open = String(open);
launch.setAttribute('aria-expanded', String(open));
if (open) input.focus(); else launch.focus();
}
function add(text, who) {
var el = document.createElement('div');
el.className = 'cb-msg ' + (who === 'user' ? 'cb-user' : 'cb-bot');
el.textContent = text; // textContent, never innerHTML
log.appendChild(el);
log.scrollTop = log.scrollHeight;
return el;
}
launch.addEventListener('click', function () { toggle(panel.hidden); });
close.addEventListener('click', function () { toggle(false); });
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !panel.hidden) toggle(false);
});
form.addEventListener('submit', function (e) {
e.preventDefault();
var text = input.value.trim();
if (!text) return;
add(text, 'user');
input.value = '';
send(text);
});
// Point this at your own endpoint, or at ChatBrace.
// Never send conversation history from the client — the server owns it.
function send(message) {
var pending = add('…', 'bot');
fetch('https://api.chatbrace.com/api/widget/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
widgetKey: 'YOUR_WIDGET_KEY',
conversationId: conversationId,
message: message
})
})
.then(function (r) { return r.json(); })
.then(function (data) {
conversationId = data.conversationId || conversationId;
pending.textContent = data.reply || 'Sorry — something went wrong.';
})
.catch(function () {
pending.textContent = 'Sorry — I could not reach the server.';
});
}
})();
</script>Put it on your site
before you finish your coffee.
A hundred free messages, no card, and a playground to try it in before you touch a single line of your own markup.