Paper
Warm off-white with ink type. The light-mode default.
Live preview · real tokens, not a screenshot
When to pick it
Most of the web is still light, and a dark widget on a white page reads as a foreign object. Paper is an off-white surface rather than pure white, which stops it glaring against body copy, with near-black ink type and a single dark accent.
How it is built
Off-white #f4f2ee rather than #ffffff: on a white page the slight warmth separates the panel from the background without needing a heavy shadow. Borders do the work a shadow would otherwise do.
Accessibility
Ink on paper measures 15.8:1. The accent doubles as the user-bubble fill with white text at 14.9:1 — one of the highest-contrast designs in the gallery.
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
- Light sites
- Editorial
- Professional services
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.
<!-- Paper — 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:#f4f2ee; --surface:#ffffff; --border:#e2ded6;
--text:#1c1b18; --muted:#6b655c; --accent:#1c1b18;
--on-accent:#f4f2ee; --bot-bg:#efece6; --bot-text:#1c1b18;
--radius:14px; --bubble:12px;
--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.