Skip to content
Corner bubbleSource included

Graphite

A neutral dark bubble that disappears into any dark site.

NNorthwind SupplyAI assistant
Do you ship to Germany?09:41
Yes — standard delivery is 3–5 working days, and free over €60.09:41

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-chatbox.html6.6 kb
<!-- 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">&times;</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">&uarr;</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>

Other corner bubble designs

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.