Open demo

CSS code

HTML
<section class="dk"><span class="lbl">Neon outline bubbles</span>
<div class="dk-neon">
  <p class="dk-say"><b>Kai</b>Raid starts at 21:00 server time. Bring fire resistance, the second boss melts tanks.</p>
  <p class="dk-say me"><b><span class="sr-only">You, </span>Nova</b>On it. I'll craft six potions and meet you at the north gate.</p>
</div></section>

<section class="dk"><span class="lbl">Gradient chat bubbles with matching tails</span>
<div class="dk-chat" role="group" aria-label="Chat with Leo Park">
  <p class="dk-msg"><span class="sr-only">Leo: </span>Did the 2.4 build pass on the night run?<time datetime="2026-10-19T07:58">07:58</time></p>
  <p class="dk-msg me"><span class="sr-only">You: </span>Green on all 312 tests. Deploying to staging now.<time datetime="2026-10-19T08:03">08:03</time></p>
</div></section>

<section class="dk"><span class="lbl">Annotation bubble on a code block</span>
<figure class="dk-code">
  <figcaption>fetch-with-retry.ts</figcaption>
  <div class="dk-body" style="--ln:5">
<pre tabindex="0" role="region" aria-label="fetch-with-retry.ts source"><code><span><span class="k">export async function</span> <span class="f">fetchWithRetry</span>(url: string, tries = <span class="n">3</span>) {</span><span>  <span class="k">for</span> (<span class="k">let</span> attempt = <span class="n">1</span>; attempt <= tries; attempt++) {</span><span>    <span class="k">const</span> res = <span class="k">await</span> <span class="f">fetch</span>(url);</span><span>    <span class="k">if</span> (res.ok) <span class="k">return</span> res.<span class="f">json</span>();</span><span class="hl">    <span class="k">await</span> <span class="f">sleep</span>(<span class="n">2</span> ** attempt * <span class="n">250</span>);</span><span>  }</span><span>  <span class="k">throw new</span> <span class="f">Error</span>(<span class="s">`Gave up after ${tries} tries`</span>);</span><span>}</span></code></pre>
    <p class="dk-note"><span class="sr-only">Note on line 5: </span>Backs off 500 ms, 1 s, then 2 s. Add jitter if thousands of clients retry at once.</p>
  </div>
</figure></section>
CSS
/* Dark speech bubbles. Light-theme bubbles turn to grey mush on a dark page, so these lean on outlines with a soft
   glow, a gradient that the tail matches, and one warm accent for annotations. */
.dk{--dk-bg:#0b0e14;--dk-panel:#141923;--dk-ink:#e6e9f2;--dk-muted:#9aa3b8;--dk-cyan:#22d3ee;--dk-pink:#f472b6;--dk-amber:#fbbf24}
.dk .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}

/* 1. Neon outline bubbles: rotated-square tails share the border, drop-shadow glows round bubble and tail together */
.dk-neon{display:flex;flex-direction:column;gap:30px;padding:30px 26px;border:1px solid #232a38;border-radius:20px;background:var(--dk-panel)}
.dk-say{--c:var(--dk-cyan);position:relative;width:fit-content;max-width:min(80%,420px);margin:0 0 0 18px;padding:14px 18px;border:1.5px solid var(--c);border-radius:16px;
  background:#0f131c;color:var(--dk-ink);font-size:.95rem;line-height:1.45;filter:drop-shadow(0 0 7px color-mix(in srgb,var(--c) 45%,transparent))}
.dk-say::after{content:"";position:absolute;left:0;bottom:16px;width:14px;height:14px;border:solid var(--c);border-width:0 0 1.5px 1.5px;background:#0f131c;translate:-50% 0;rotate:45deg}
.dk-say.me{--c:var(--dk-pink);align-self:flex-end;margin:0 18px 0 0}
.dk-say.me::after{left:100%;border-width:1.5px 1.5px 0 0}
.dk-say b{display:block;margin-bottom:2px;font-size:.75rem;letter-spacing:.06em;text-transform:uppercase;color:var(--c)}

/* 2. Gradient chat bubbles: the tail takes the gradient's last colour, so the join is invisible */
.dk-chat{display:flex;flex-direction:column;gap:12px;padding:24px 26px;border:1px solid #232a38;border-radius:20px;background:var(--dk-panel)}
.dk-msg{position:relative;width:fit-content;max-width:75%;margin:0;padding:10px 15px;border-radius:18px 18px 18px 4px;background:#252c3b;color:var(--dk-ink);font-size:.93rem;line-height:1.4}
.dk-msg::after{content:"";position:absolute;bottom:0;left:-7px;width:15px;height:16px;background:radial-gradient(7px 16px at 0 0,transparent 96%,#252c3b 100%)}
.dk-msg.me{align-self:flex-end;border-radius:18px 18px 4px 18px;background:linear-gradient(160deg,#8b5cf6,#4f46e5);color:#fff}
.dk-msg.me::after{left:auto;right:-7px;background:radial-gradient(7px 16px at 100% 0,transparent 96%,#4f46e5 100%)}
.dk-msg time{display:block;margin-top:2px;font-size:.7rem;color:var(--dk-muted)}
.dk-msg.me time{color:#ddd6fe}

/* 3. Code annotation: an amber bubble pinned to line --ln of the block, tail pointing back at the code */
.dk-code{margin:0;border:1px solid #232a38;border-radius:16px;background:var(--dk-panel);overflow:hidden}
.dk-code figcaption{padding:10px 16px;border-bottom:1px solid #232a38;font:600 .78rem/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--dk-muted)}
.dk-body{--ln:5;--lh:1.75em;position:relative;font:400 .84rem/var(--lh) ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.dk-body pre{margin:0;padding:16px 0;overflow-x:auto;font:inherit;counter-reset:ln}
.dk-body pre:focus-visible{outline:2px solid var(--dk-amber);outline-offset:-2px}
.dk-body code{display:block;width:max-content;min-width:100%;font:inherit;color:#d6dbe7}
.dk-body code>span{display:block;padding:0 18px 0 0}
.dk-body code>span::before{counter-increment:ln;content:counter(ln);display:inline-block;width:3em;margin-right:14px;padding-right:10px;text-align:right;color:#8a93a8}
.dk-body code>span.hl{background:rgba(251,191,36,.12);box-shadow:inset 3px 0 var(--dk-amber)}
.k{color:#d4a5ff}.f{color:#8fb8ff}.s{color:#a7e3a1}.n{color:#ffab85}
.dk-note{position:absolute;right:16px;top:calc(16px + (var(--ln) - 1) * var(--lh) + var(--lh) / 2);translate:0 -50%;width:230px;margin:0;padding:10px 13px;border-radius:10px;
  background:var(--dk-amber);color:#1f1500;font:500 .8rem/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;box-shadow:0 10px 24px -10px rgba(0,0,0,.7)}
.dk-note::after{content:"";position:absolute;right:100%;top:50%;margin-top:-8px;border:8px solid transparent;border-left-width:0;border-right-color:var(--dk-amber)}
@media (max-width:720px){
  .dk-note{position:relative;right:auto;top:auto;translate:none;width:auto;margin:4px 14px 16px}
  .dk-note::after{right:auto;left:28px;top:auto;bottom:100%;margin:0;border-width:0 8px 8px;border-color:transparent transparent var(--dk-amber)}
}
@media (max-width:480px){.dk-say{max-width:88%}.dk-msg{max-width:85%}.dk-neon,.dk-chat{padding:22px 16px}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<section class="dk" data-bs-theme="dark"><span class="lbl">Neon outline bubbles</span>
<div class="dk-neon card">
  <p class="dk-say mb-0"><b class="d-block text-uppercase small">Kai</b>Raid starts at 21:00 server time. Bring fire resistance, the second boss melts tanks.</p>
  <p class="dk-say me mb-0"><b class="d-block text-uppercase small"><span class="visually-hidden">You, </span>Nova</b>On it. I'll craft six potions and meet you at the north gate.</p>
</div></section>

<section class="dk" data-bs-theme="dark"><span class="lbl">Gradient chat bubbles with matching tails</span>
<div class="dk-chat card" role="group" aria-label="Chat with Leo Park">
  <p class="dk-msg"><span class="visually-hidden">Leo: </span>Did the 2.4 build pass on the night run?<time class="d-block small" datetime="2026-10-19T07:58">07:58</time></p>
  <p class="dk-msg me"><span class="visually-hidden">You: </span>Green on all 312 tests. Deploying to staging now.<time class="d-block small" datetime="2026-10-19T08:03">08:03</time></p>
</div></section>

<section class="dk" data-bs-theme="dark"><span class="lbl">Annotation bubble on a code block</span>
<figure class="dk-code card mb-0">
  <figcaption class="card-header">fetch-with-retry.ts</figcaption>
  <div class="dk-body" style="--ln:5">
<pre tabindex="0" role="region" aria-label="fetch-with-retry.ts source"><code><span><span class="k">export async function</span> <span class="f">fetchWithRetry</span>(url: string, tries = <span class="n">3</span>) {</span><span>  <span class="k">for</span> (<span class="k">let</span> attempt = <span class="n">1</span>; attempt <= tries; attempt++) {</span><span>    <span class="k">const</span> res = <span class="k">await</span> <span class="f">fetch</span>(url);</span><span>    <span class="k">if</span> (res.ok) <span class="k">return</span> res.<span class="f">json</span>();</span><span class="hl">    <span class="k">await</span> <span class="f">sleep</span>(<span class="n">2</span> ** attempt * <span class="n">250</span>);</span><span>  }</span><span>  <span class="k">throw new</span> <span class="f">Error</span>(<span class="s">`Gave up after ${tries} tries`</span>);</span><span>}</span></code></pre>
    <p class="dk-note"><span class="visually-hidden">Note on line 5: </span>Backs off 500 ms, 1 s, then 2 s. Add jitter if thousands of clients retry at once.</p>
  </div>
</figure></section>
CSS
/* Dark speech bubbles on Bootstrap in data-bs-theme="dark" (.card panels, .card-header, .visually-hidden). Light-theme bubbles turn to grey mush on a dark page, so these lean on outlines with a soft
   glow, a gradient that the tail matches, and one warm accent for annotations. */
.dk{--bs-card-bg:var(--dk-panel);--bs-card-border-color:#232a38;--bs-card-cap-bg:transparent;--bs-card-border-radius:20px;--dk-bg:#0b0e14;--dk-panel:#141923;--dk-ink:#e6e9f2;--dk-muted:#9aa3b8;--dk-cyan:#22d3ee;--dk-pink:#f472b6;--dk-amber:#fbbf24}

/* 1. Neon outline bubbles: rotated-square tails share the border, drop-shadow glows round bubble and tail together */
.dk-neon{display:flex;flex-direction:column;gap:30px;padding:30px 26px;border:1px solid #232a38;border-radius:20px;background:var(--dk-panel)}
.dk-say{--c:var(--dk-cyan);position:relative;width:fit-content;max-width:min(80%,420px);margin:0 0 0 18px;padding:14px 18px;border:1.5px solid var(--c);border-radius:16px;
  background:#0f131c;color:var(--dk-ink);font-size:.95rem;line-height:1.45;filter:drop-shadow(0 0 7px color-mix(in srgb,var(--c) 45%,transparent))}
.dk-say::after{content:"";position:absolute;left:0;bottom:16px;width:14px;height:14px;border:solid var(--c);border-width:0 0 1.5px 1.5px;background:#0f131c;translate:-50% 0;rotate:45deg}
.dk-say.me{--c:var(--dk-pink);align-self:flex-end;margin:0 18px 0 0}
.dk-say.me::after{left:100%;border-width:1.5px 1.5px 0 0}
.dk-say b{display:block;margin-bottom:2px;font-size:.75rem;letter-spacing:.06em;text-transform:uppercase;color:var(--c)}

/* 2. Gradient chat bubbles: the tail takes the gradient's last colour, so the join is invisible */
.dk-chat{display:flex;flex-direction:column;gap:12px;padding:24px 26px;border:1px solid #232a38;border-radius:20px;background:var(--dk-panel)}
.dk-msg{position:relative;width:fit-content;max-width:75%;margin:0;padding:10px 15px;border-radius:18px 18px 18px 4px;background:#252c3b;color:var(--dk-ink);font-size:.93rem;line-height:1.4}
.dk-msg::after{content:"";position:absolute;bottom:0;left:-7px;width:15px;height:16px;background:radial-gradient(7px 16px at 0 0,transparent 96%,#252c3b 100%)}
.dk-msg.me{align-self:flex-end;border-radius:18px 18px 4px 18px;background:linear-gradient(160deg,#8b5cf6,#4f46e5);color:#fff}
.dk-msg.me::after{left:auto;right:-7px;background:radial-gradient(7px 16px at 100% 0,transparent 96%,#4f46e5 100%)}
.dk-msg time{display:block;margin-top:2px;font-size:.7rem;color:var(--dk-muted)}
.dk-msg.me time{color:#ddd6fe}

/* 3. Code annotation: an amber bubble pinned to line --ln of the block, tail pointing back at the code */
.dk-code{margin:0;border:1px solid #232a38;border-radius:16px;background:var(--dk-panel);overflow:hidden}
.dk-code figcaption{padding:10px 16px;border-bottom:1px solid #232a38;font:600 .78rem/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--dk-muted)}
.dk-body{--ln:5;--lh:1.75em;position:relative;font:400 .84rem/var(--lh) ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.dk-body pre{margin:0;padding:16px 0;overflow-x:auto;font:inherit;counter-reset:ln}
.dk-body pre:focus-visible{outline:2px solid var(--dk-amber);outline-offset:-2px}
.dk-body code{display:block;width:max-content;min-width:100%;font:inherit;color:#d6dbe7}
.dk-body code>span{display:block;padding:0 18px 0 0}
.dk-body code>span::before{counter-increment:ln;content:counter(ln);display:inline-block;width:3em;margin-right:14px;padding-right:10px;text-align:right;color:#8a93a8}
.dk-body code>span.hl{background:rgba(251,191,36,.12);box-shadow:inset 3px 0 var(--dk-amber)}
.k{color:#d4a5ff}.f{color:#8fb8ff}.s{color:#a7e3a1}.n{color:#ffab85}
.dk-note{position:absolute;right:16px;top:calc(16px + (var(--ln) - 1) * var(--lh) + var(--lh) / 2);translate:0 -50%;width:230px;margin:0;padding:10px 13px;border-radius:10px;
  background:var(--dk-amber);color:#1f1500;font:500 .8rem/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;box-shadow:0 10px 24px -10px rgba(0,0,0,.7)}
.dk-note::after{content:"";position:absolute;right:100%;top:50%;margin-top:-8px;border:8px solid transparent;border-left-width:0;border-right-color:var(--dk-amber)}
@media (max-width:720px){
  .dk-note{position:relative;right:auto;top:auto;translate:none;width:auto;margin:4px 14px 16px}
  .dk-note::after{right:auto;left:28px;top:auto;bottom:100%;margin:0;border-width:0 8px 8px;border-color:transparent transparent var(--dk-amber)}
}
@media (max-width:480px){.dk-say{max-width:88%}.dk-msg{max-width:85%}.dk-neon,.dk-chat{padding:22px 16px}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<section><span class="mb-[18px] block text-[.72rem] tracking-[.12em] text-dk-muted uppercase">Neon outline bubbles</span>
<div class="rounded-[20px] border border-[#232a38] bg-dk-panel flex flex-col gap-[30px] px-[26px] py-[30px] max-[480px]:px-4 max-[480px]:py-[22px]">
  <p class="dk-say relative m-0 w-fit max-w-[min(80%,420px)] rounded-2xl border-[1.5px] border-(--c) bg-[#0f131c] px-[18px] py-3.5 text-[.95rem] leading-[1.45] text-dk-ink drop-shadow-[0_0_7px_color-mix(in_srgb,var(--c)_45%,transparent)] max-[480px]:max-w-[88%] ml-[18px] [--c:var(--color-dk-cyan)]"><b class="mb-0.5 block text-xs tracking-[.06em] text-(--c) uppercase">Kai</b>Raid starts at 21:00 server time. Bring fire resistance, the second boss melts tanks.</p>
  <p class="dk-say relative m-0 w-fit max-w-[min(80%,420px)] rounded-2xl border-[1.5px] border-(--c) bg-[#0f131c] px-[18px] py-3.5 text-[.95rem] leading-[1.45] text-dk-ink drop-shadow-[0_0_7px_color-mix(in_srgb,var(--c)_45%,transparent)] max-[480px]:max-w-[88%] dk-me mr-[18px] self-end [--c:var(--color-dk-pink)]"><b class="mb-0.5 block text-xs tracking-[.06em] text-(--c) uppercase"><span class="sr-only">You, </span>Nova</b>On it. I'll craft six potions and meet you at the north gate.</p>
</div></section>

<section><span class="mb-[18px] block text-[.72rem] tracking-[.12em] text-dk-muted uppercase">Gradient chat bubbles with matching tails</span>
<div class="rounded-[20px] border border-[#232a38] bg-dk-panel flex flex-col gap-3 px-[26px] py-6 max-[480px]:px-4 max-[480px]:py-[22px]" role="group" aria-label="Chat with Leo Park">
  <p class="dk-msg relative m-0 w-fit max-w-[75%] px-[15px] py-2.5 text-[.93rem] leading-[1.4] max-[480px]:max-w-[85%] rounded-[18px_18px_18px_4px] bg-[#252c3b] text-dk-ink"><span class="sr-only">Leo: </span>Did the 2.4 build pass on the night run?<time class="mt-0.5 block text-[.7rem] text-dk-muted" datetime="2026-10-19T07:58">07:58</time></p>
  <p class="dk-msg relative m-0 w-fit max-w-[75%] px-[15px] py-2.5 text-[.93rem] leading-[1.4] max-[480px]:max-w-[85%] dk-me self-end rounded-[18px_18px_4px_18px] bg-linear-160 from-[#8b5cf6] to-[#4f46e5] text-white"><span class="sr-only">You: </span>Green on all 312 tests. Deploying to staging now.<time class="mt-0.5 block text-[.7rem] text-[#ddd6fe]" datetime="2026-10-19T08:03">08:03</time></p>
</div></section>

<section><span class="mb-[18px] block text-[.72rem] tracking-[.12em] text-dk-muted uppercase">Annotation bubble on a code block</span>
<figure class="m-0 overflow-hidden rounded-2xl border border-[#232a38] bg-dk-panel">
  <figcaption class="border-b border-[#232a38] px-4 py-2.5 font-mono text-[.78rem] leading-[1.4] font-semibold text-dk-muted">fetch-with-retry.ts</figcaption>
  <div class="dk-body relative font-mono text-[.84rem] leading-(--lh) [--lh:1.75em] [--ln:5]">
<pre class="m-0 overflow-x-auto py-4 [counter-reset:ln] focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-dk-amber" tabindex="0" role="region" aria-label="fetch-with-retry.ts source"><code class="block w-max min-w-full text-[#d6dbe7]"><span class="dk-ln block pr-[18px]"><span class="text-[#d4a5ff]">export async function</span> <span class="text-[#8fb8ff]">fetchWithRetry</span>(url: string, tries = <span class="text-[#ffab85]">3</span>) {</span><span class="dk-ln block pr-[18px]">  <span class="text-[#d4a5ff]">for</span> (<span class="text-[#d4a5ff]">let</span> attempt = <span class="text-[#ffab85]">1</span>; attempt <= tries; attempt++) {</span><span class="dk-ln block pr-[18px]">    <span class="text-[#d4a5ff]">const</span> res = <span class="text-[#d4a5ff]">await</span> <span class="text-[#8fb8ff]">fetch</span>(url);</span><span class="dk-ln block pr-[18px]">    <span class="text-[#d4a5ff]">if</span> (res.ok) <span class="text-[#d4a5ff]">return</span> res.<span class="text-[#8fb8ff]">json</span>();</span><span class="dk-ln block bg-[rgba(251,191,36,.12)] pr-[18px] shadow-[inset_3px_0_var(--color-dk-amber)]">    <span class="text-[#d4a5ff]">await</span> <span class="text-[#8fb8ff]">sleep</span>(<span class="text-[#ffab85]">2</span> ** attempt * <span class="text-[#ffab85]">250</span>);</span><span class="dk-ln block pr-[18px]">  }</span><span class="dk-ln block pr-[18px]">  <span class="text-[#d4a5ff]">throw new</span> <span class="text-[#8fb8ff]">Error</span>(<span class="text-[#a7e3a1]">`Gave up after ${tries} tries`</span>);</span><span class="dk-ln block pr-[18px]">}</span></code></pre>
    <p class="dk-note absolute top-[calc(16px+(var(--ln)-1)*var(--lh)+var(--lh)/2)] right-4 m-0 w-[230px] -translate-y-1/2 rounded-[10px] bg-dk-amber px-[13px] py-2.5 font-sans text-[.8rem] leading-[1.45] font-medium text-[#1f1500] shadow-[0_10px_24px_-10px_rgba(0,0,0,.7)] max-[720px]:relative max-[720px]:top-auto max-[720px]:right-auto max-[720px]:mx-3.5 max-[720px]:mt-1 max-[720px]:mb-4 max-[720px]:w-auto max-[720px]:translate-y-0"><span class="sr-only">Note on line 5: </span>Backs off 500 ms, 1 s, then 2 s. Add jitter if thousands of clients retry at once.</p>
  </div>
</figure></section>
CSS
@theme {
  --color-dk-panel:#141923; --color-dk-ink:#e6e9f2; --color-dk-muted:#9aa3b8;
  --color-dk-cyan:#22d3ee; --color-dk-pink:#f472b6; --color-dk-amber:#fbbf24;
}
/* Utilities draw the bubbles; tails and line numbers are repeated pseudo-elements, so they live here. */
.dk-say::after{content:"";position:absolute;left:0;bottom:16px;width:14px;height:14px;border:solid var(--c);border-width:0 0 1.5px 1.5px;background:#0f131c;translate:-50% 0;rotate:45deg}
.dk-say.dk-me::after{left:100%;border-width:1.5px 1.5px 0 0}
.dk-msg::after{content:"";position:absolute;bottom:0;left:-7px;width:15px;height:16px;background:radial-gradient(7px 16px at 0 0,transparent 96%,#252c3b 100%)}
.dk-msg.dk-me::after{left:auto;right:-7px;background:radial-gradient(7px 16px at 100% 0,transparent 96%,#4f46e5 100%)}
.dk-ln::before{counter-increment:ln;content:counter(ln);display:inline-block;width:3em;margin-right:14px;padding-right:10px;text-align:right;color:#8a93a8}
.dk-note::after{content:"";position:absolute;right:100%;top:50%;margin-top:-8px;border:8px solid transparent;border-left-width:0;border-right-color:var(--color-dk-amber)}
@media (max-width:720px){.dk-note::after{right:auto;left:28px;top:auto;bottom:100%;margin:0;border-width:0 8px 8px;border-color:transparent transparent var(--color-dk-amber)}}

About this set

Speech bubbles for dark interfaces, where a white or pale grey bubble turns into a glaring block. Neon outline bubbles gives two players in a game chat cyan and pink outlined bubbles with rotated-square tails that share the outline, and a drop-shadow glow mixed from the outline colour with color-mix(), which follows the bubble and its tail together. Each bubble starts with the player’s name in the outline colour. Gradient chat bubbles with matching tails sets sent messages on a violet-to-indigo gradient and received ones on slate. The tail is a radial-gradient cut-out in the gradient’s last colour, so the join is invisible. Annotation bubble on a code block pins an amber note to line five of a TypeScript retry function, with line numbers from a CSS counter and the line itself highlighted, the tail pointing back at the code. Below 720px the note moves under the code and its tail turns upward. The code block is a focusable, labelled region so keyboard users can scroll its long lines. The Bootstrap version runs in data-bs-theme=”dark”.

What’s included

  • Neon outline bubbles whose glow follows the tail
  • Gradient sent bubbles with a matching curved tail
  • Amber note pinned to a line of code with CSS line numbers
  • Keyboard-scrollable code region with a focus outline
  • Bootstrap version in data-bs-theme="dark"

Accessibility

Every text colour was checked against its own bubble at 4.5:1 or more, including the time stamps. Sender names are visible or visually hidden at the start of each message, the code note starts with hidden text Note on line 5, and the scrollable code block is a focusable region with a name and an amber focus outline. Line numbers are generated content and are not read as part of the code.

Customise it

Change --dk-cyan, --dk-pink and --dk-amber on .dk for the accents, the linear-gradient on .dk-msg.me for sent bubbles, and --ln on .dk-body to pin the note to another line. In Tailwind edit the --color-dk-* theme colours and [--ln:5]; Bootstrap uses the same variables on .card.