Graphite
A neutral dark bubble that disappears into any dark site.
Live preview · real tokens, not a screenshot
When to pick it
The default for a reason. Graphite takes no position on your brand — it is a cool near-black panel with a single light accent, so it sits on a dark marketing site without competing with anything already on the page. If you are unsure which design to start with, start here and change the accent colour to your brand's.
How it is built
A 12px radius panel over a 1px hairline border, with an inset top highlight at 4% white to suggest a milled edge. The whole surface is one solid colour — no blur — so it costs nothing to composite and never inherits a backdrop.
Accessibility
Body text sits at 13.2:1 against the panel and the muted timestamp at 5.1:1, both well past AA. The accent is used for surfaces, not for text on dark.
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
- Dark marketing sites
- Developer tools
- Documentation
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.
<!-- Graphite — 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:#16181c; --surface:#1e2126; --border:#2b2f36;
--text:#e8eaed; --muted:#9aa1ab; --accent:#e8eaed;
--on-accent:#16181c; --bot-bg:#22262c; --bot-text:#e8eaed;
--radius:12px; --bubble:10px;
--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:0 8px 24px -8px rgba(0,0,0,.35); 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:0 8px 24px -8px rgba(0,0,0,.35); 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);
}
#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.