Open demo

CSS code

HTML
<section><span class="lbl">Tails on four sides</span>
<div class="sb-grid">
  <div class="sb-cell"><p class="sb sb-top">Your table for two is confirmed for Friday at 19:30.</p><code class="cap">.sb-top</code></div>
  <div class="sb-cell"><p class="sb sb-right">Leave the parcel with the neighbour at number 14.</p><code class="cap">.sb-right</code></div>
  <div class="sb-cell"><p class="sb sb-bottom">Sunset at the pier is 18:52 tonight. Bring a jacket.</p><code class="cap">.sb-bottom</code></div>
  <div class="sb-cell"><p class="sb sb-left">I've put the Lisbon receipts in the shared folder.</p><code class="cap">.sb-left</code></div>
</div></section>

<section><span class="lbl">Corner tails</span>
<div class="sb-grid">
  <div class="sb-cell"><p class="sb sb-tl sb-sun">Can we push stand-up to 10:15?</p><code class="cap">.sb-tl</code></div>
  <div class="sb-cell"><p class="sb sb-tr sb-teal">Sure, the room is free until noon.</p><code class="cap">.sb-tr</code></div>
  <div class="sb-cell"><p class="sb sb-bl sb-sun">Who has the keys to the van?</p><code class="cap">.sb-bl</code></div>
  <div class="sb-cell"><p class="sb sb-br sb-teal">Pilar does. She's parked by gate B.</p><code class="cap">.sb-br</code></div>
</div></section>

<section><span class="lbl">Bordered bubbles with matching tail borders</span>
<div class="sb-grid">
  <div class="sb-cell"><p class="sb sb-line sb-top">Step 2 of 3: choose a delivery slot.</p><code class="cap">.sb-line.sb-top</code></div>
  <div class="sb-cell"><p class="sb sb-line sb-right">Prices include VAT and the €4 cleaning fee.</p><code class="cap">.sb-line.sb-right</code></div>
  <div class="sb-cell"><p class="sb sb-line sb-bottom" style="--sb-pos:30px">Hold Shift to select a range of days.</p><code class="cap">--sb-pos: 30px</code></div>
  <div class="sb-cell"><p class="sb sb-line sb-left">Drafts are saved every 30 seconds.</p><code class="cap">.sb-line.sb-left</code></div>
</div></section>
CSS
/* Classic speech bubbles. The tail is the bubble's ::after: a zero-size box whose one coloured border is a triangle.
   --sb-c is the bubble colour (the tail reads it too), --sb-t the tail size, --sb-pos where the tail sits along its edge. */
.sb{--sb-c:#2848c9;--sb-ink:#fff;--sb-t:13px;--sb-pos:50%;--sb-r:16px;
  position:relative;width:fit-content;max-width:100%;margin:0;padding:16px;border-radius:var(--sb-r);
  background:var(--sb-c);color:var(--sb-ink);font-size:.95rem;line-height:1.5;filter:drop-shadow(0 8px 14px rgba(31,36,48,.14))}
.sb::after{content:"";position:absolute;width:0;height:0;border:var(--sb-t) solid transparent}

/* Tails on the four sides, centred by default */
.sb-top::after{bottom:100%;left:var(--sb-pos);margin-left:calc(var(--sb-t) * -1);border-top-width:0;border-bottom-color:var(--sb-c)}
.sb-bottom::after{top:100%;left:var(--sb-pos);margin-left:calc(var(--sb-t) * -1);border-bottom-width:0;border-top-color:var(--sb-c)}
.sb-left::after{right:100%;top:var(--sb-pos);margin-top:calc(var(--sb-t) * -1);border-left-width:0;border-right-color:var(--sb-c)}
.sb-right::after{left:100%;top:var(--sb-pos);margin-top:calc(var(--sb-t) * -1);border-right-width:0;border-left-color:var(--sb-c)}

/* Corner tails: the corner loses its radius and a right-angled triangle carries the edge on */
.sb-tl,.sb-tr,.sb-bl,.sb-br{--sb-t:14px}
.sb-tl{border-top-left-radius:0}
.sb-tl::after{bottom:100%;left:0;border-width:0 var(--sb-t) var(--sb-t) 0;border-bottom-color:var(--sb-c)}
.sb-tr{border-top-right-radius:0}
.sb-tr::after{bottom:100%;right:0;border-width:0 0 var(--sb-t) var(--sb-t);border-bottom-color:var(--sb-c)}
.sb-bl{border-bottom-left-radius:0}
.sb-bl::after{top:100%;left:0;border-width:var(--sb-t) var(--sb-t) 0 0;border-top-color:var(--sb-c)}
.sb-br{border-bottom-right-radius:0}
.sb-br::after{top:100%;right:0;border-width:var(--sb-t) 0 0 var(--sb-t);border-top-color:var(--sb-c)}

/* Colour themes */
.sb-sun{--sb-c:#ffd36e;--sb-ink:#3a2a00}
.sb-teal{--sb-c:#0f6e66;--sb-ink:#fff}

/* Bordered bubbles: the tail is a rotated square with the same border on its two outer sides,
   painted over the bubble's own border so the outline runs unbroken into the point. */
.sb-line{--sb-c:#fff;--sb-ink:#1f2430;--sb-line:#1f2430;--sb-bw:2px;--sb-s:18px;border:var(--sb-bw) solid var(--sb-line);filter:drop-shadow(4px 5px 0 rgba(31,36,48,.16))}
.sb-line::after{width:var(--sb-s);height:var(--sb-s);margin:0;border:0 solid var(--sb-line);background:var(--sb-c);translate:-50% -50%;rotate:45deg}
.sb-line.sb-top::after{top:0;bottom:auto;left:var(--sb-pos);border-top-width:var(--sb-bw);border-left-width:var(--sb-bw)}
.sb-line.sb-bottom::after{top:100%;left:var(--sb-pos);border-right-width:var(--sb-bw);border-bottom-width:var(--sb-bw)}
.sb-line.sb-left::after{left:0;right:auto;top:var(--sb-pos);border-bottom-width:var(--sb-bw);border-left-width:var(--sb-bw)}
.sb-line.sb-right::after{left:100%;top:var(--sb-pos);border-top-width:var(--sb-bw);border-right-width:var(--sb-bw)}

/* Demo layout: four bubbles in a row, two on tablets, one on phones */
.sb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:44px 36px;align-items:start}
.sb-cell{display:flex;flex-direction:column;align-items:center;gap:26px;margin:0;padding:0 14px}
.sb-cell .sb{max-width:250px}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<section><span class="lbl">Tails on four sides</span>
<div class="row gy-5 gx-4 align-items-start">
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-top">Your table for two is confirmed for Friday at 19:30.</p><code class="cap fw-semibold small">.sb-top</code></div>
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-right">Leave the parcel with the neighbour at number 14.</p><code class="cap fw-semibold small">.sb-right</code></div>
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-bottom">Sunset at the pier is 18:52 tonight. Bring a jacket.</p><code class="cap fw-semibold small">.sb-bottom</code></div>
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-left">I've put the Lisbon receipts in the shared folder.</p><code class="cap fw-semibold small">.sb-left</code></div>
</div></section>

<section><span class="lbl">Corner tails</span>
<div class="row gy-5 gx-4 align-items-start">
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-tl sb-sun">Can we push stand-up to 10:15?</p><code class="cap fw-semibold small">.sb-tl</code></div>
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-tr sb-teal">Sure, the room is free until noon.</p><code class="cap fw-semibold small">.sb-tr</code></div>
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-bl sb-sun">Who has the keys to the van?</p><code class="cap fw-semibold small">.sb-bl</code></div>
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-br sb-teal">Pilar does. She's parked by gate B.</p><code class="cap fw-semibold small">.sb-br</code></div>
</div></section>

<section><span class="lbl">Bordered bubbles with matching tail borders</span>
<div class="row gy-5 gx-4 align-items-start">
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-line sb-top">Step 2 of 3: choose a delivery slot.</p><code class="cap fw-semibold small">.sb-line.sb-top</code></div>
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-line sb-right">Prices include VAT and the €4 cleaning fee.</p><code class="cap fw-semibold small">.sb-line.sb-right</code></div>
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-line sb-bottom" style="--sb-pos:30px">Hold Shift to select a range of days.</p><code class="cap fw-semibold small">--sb-pos: 30px</code></div>
  <div class="col-sm-6 col-lg-3 d-flex flex-column align-items-center gap-4 px-4"><p class="sb p-3 rounded-4 mb-0 sb-line sb-left">Drafts are saved every 30 seconds.</p><code class="cap fw-semibold small">.sb-line.sb-left</code></div>
</div></section>
CSS
/* Classic speech bubbles on Bootstrap: .p-3, .rounded-4 and the grid do the box, this block draws the tails.
   The tail is the bubble's ::after: a zero-size box whose one coloured border is a triangle.
   --sb-c is the bubble colour (the tail reads it too), --sb-t the tail size, --sb-pos where the tail sits along its edge. */
.sb{--sb-c:#2848c9;--sb-ink:#fff;--sb-t:13px;--sb-pos:50%;--sb-r:var(--bs-border-radius-xl);
  position:relative;max-width:250px;
  background:var(--sb-c);color:var(--sb-ink);font-size:.95rem;line-height:1.5;filter:drop-shadow(0 8px 14px rgba(31,36,48,.14))}
.sb::after{content:"";position:absolute;width:0;height:0;border:var(--sb-t) solid transparent}

/* Tails on the four sides, centred by default */
.sb-top::after{bottom:100%;left:var(--sb-pos);margin-left:calc(var(--sb-t) * -1);border-top-width:0;border-bottom-color:var(--sb-c)}
.sb-bottom::after{top:100%;left:var(--sb-pos);margin-left:calc(var(--sb-t) * -1);border-bottom-width:0;border-top-color:var(--sb-c)}
.sb-left::after{right:100%;top:var(--sb-pos);margin-top:calc(var(--sb-t) * -1);border-left-width:0;border-right-color:var(--sb-c)}
.sb-right::after{left:100%;top:var(--sb-pos);margin-top:calc(var(--sb-t) * -1);border-right-width:0;border-left-color:var(--sb-c)}

/* Corner tails (!important beats .rounded-4): the corner loses its radius and a right-angled triangle carries the edge on */
.sb-tl,.sb-tr,.sb-bl,.sb-br{--sb-t:14px}
.sb-tl{border-top-left-radius:0!important}
.sb-tl::after{bottom:100%;left:0;border-width:0 var(--sb-t) var(--sb-t) 0;border-bottom-color:var(--sb-c)}
.sb-tr{border-top-right-radius:0!important}
.sb-tr::after{bottom:100%;right:0;border-width:0 0 var(--sb-t) var(--sb-t);border-bottom-color:var(--sb-c)}
.sb-bl{border-bottom-left-radius:0!important}
.sb-bl::after{top:100%;left:0;border-width:var(--sb-t) var(--sb-t) 0 0;border-top-color:var(--sb-c)}
.sb-br{border-bottom-right-radius:0!important}
.sb-br::after{top:100%;right:0;border-width:var(--sb-t) 0 0 var(--sb-t);border-top-color:var(--sb-c)}

/* Colour themes */
.sb-sun{--sb-c:#ffd36e;--sb-ink:#3a2a00}
.sb-teal{--sb-c:#0f6e66;--sb-ink:#fff}

/* Bordered bubbles: the tail is a rotated square with the same border on its two outer sides,
   painted over the bubble's own border so the outline runs unbroken into the point. */
.sb-line{--sb-c:#fff;--sb-ink:#1f2430;--sb-line:#1f2430;--sb-bw:2px;--sb-s:18px;border:var(--sb-bw) solid var(--sb-line);filter:drop-shadow(4px 5px 0 rgba(31,36,48,.16))}
.sb-line::after{width:var(--sb-s);height:var(--sb-s);margin:0;border:0 solid var(--sb-line);background:var(--sb-c);translate:-50% -50%;rotate:45deg}
.sb-line.sb-top::after{top:0;bottom:auto;left:var(--sb-pos);border-top-width:var(--sb-bw);border-left-width:var(--sb-bw)}
.sb-line.sb-bottom::after{top:100%;left:var(--sb-pos);border-right-width:var(--sb-bw);border-bottom-width:var(--sb-bw)}
.sb-line.sb-left::after{left:0;right:auto;top:var(--sb-pos);border-bottom-width:var(--sb-bw);border-left-width:var(--sb-bw)}
.sb-line.sb-right::after{left:100%;top:var(--sb-pos);border-top-width:var(--sb-bw);border-right-width:var(--sb-bw)}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<section><span class="mb-[22px] block text-[.72rem] tracking-[.12em] text-sb-muted uppercase">Tails on four sides</span>
<div class="grid grid-cols-[repeat(auto-fit,minmax(210px,1fr))] items-start gap-x-9 gap-y-11">
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-top drop-shadow-[0_8px_14px_rgba(31,36,48,.14)] [--sb-c:var(--color-sb-blue)] text-white relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Your table for two is confirmed for Friday at 19:30.</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-top</code></div>
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-right drop-shadow-[0_8px_14px_rgba(31,36,48,.14)] [--sb-c:var(--color-sb-blue)] text-white relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Leave the parcel with the neighbour at number 14.</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-right</code></div>
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-bottom drop-shadow-[0_8px_14px_rgba(31,36,48,.14)] [--sb-c:var(--color-sb-blue)] text-white relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Sunset at the pier is 18:52 tonight. Bring a jacket.</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-bottom</code></div>
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-left drop-shadow-[0_8px_14px_rgba(31,36,48,.14)] [--sb-c:var(--color-sb-blue)] text-white relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">I've put the Lisbon receipts in the shared folder.</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-left</code></div>
</div></section>

<section><span class="mb-[22px] block text-[.72rem] tracking-[.12em] text-sb-muted uppercase">Corner tails</span>
<div class="grid grid-cols-[repeat(auto-fit,minmax(210px,1fr))] items-start gap-x-9 gap-y-11">
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-tl drop-shadow-[0_8px_14px_rgba(31,36,48,.14)] [--sb-c:var(--color-sb-sun)] text-[#3a2a00] rounded-tl-none relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Can we push stand-up to 10:15?</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-tl</code></div>
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-tr drop-shadow-[0_8px_14px_rgba(31,36,48,.14)] [--sb-c:var(--color-sb-teal)] text-white rounded-tr-none relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Sure, the room is free until noon.</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-tr</code></div>
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-bl drop-shadow-[0_8px_14px_rgba(31,36,48,.14)] [--sb-c:var(--color-sb-sun)] text-[#3a2a00] rounded-bl-none relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Who has the keys to the van?</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-bl</code></div>
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-br drop-shadow-[0_8px_14px_rgba(31,36,48,.14)] [--sb-c:var(--color-sb-teal)] text-white rounded-br-none relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Pilar does. She's parked by gate B.</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-br</code></div>
</div></section>

<section><span class="mb-[22px] block text-[.72rem] tracking-[.12em] text-sb-muted uppercase">Bordered bubbles with matching tail borders</span>
<div class="grid grid-cols-[repeat(auto-fit,minmax(210px,1fr))] items-start gap-x-9 gap-y-11">
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-top sb-line border-2 border-sb-ink text-sb-ink [--sb-c:#fff] drop-shadow-[4px_5px_0_rgba(31,36,48,.16)] relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Step 2 of 3: choose a delivery slot.</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-line.sb-top</code></div>
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-right sb-line border-2 border-sb-ink text-sb-ink [--sb-c:#fff] drop-shadow-[4px_5px_0_rgba(31,36,48,.16)] relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Prices include VAT and the €4 cleaning fee.</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-line.sb-right</code></div>
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="[--sb-pos:30px] sb-bottom sb-line border-2 border-sb-ink text-sb-ink [--sb-c:#fff] drop-shadow-[4px_5px_0_rgba(31,36,48,.16)] relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Hold Shift to select a range of days.</p><code class="font-mono text-xs font-semibold text-sb-muted">--sb-pos: 30px</code></div>
  <div class="flex flex-col items-center gap-[26px] px-3.5"><p class="sb-left sb-line border-2 border-sb-ink text-sb-ink [--sb-c:#fff] drop-shadow-[4px_5px_0_rgba(31,36,48,.16)] relative m-0 w-fit max-w-[250px] rounded-2xl p-4 text-[.95rem] leading-normal bg-(--sb-c)">Drafts are saved every 30 seconds.</p><code class="font-mono text-xs font-semibold text-sb-muted">.sb-line.sb-left</code></div>
</div></section>
CSS
@theme {
  --color-sb-blue:#2848c9; --color-sb-sun:#ffd36e; --color-sb-teal:#0f6e66; --color-sb-ink:#1f2430; --color-sb-muted:#595e6b;
}
/* Utilities draw the bubble; the tail is a repeated ::after, so it lives here. Each bubble sets --sb-c to its own
   background colour and the tail reads it. --sb-t is the tail size; set [--sb-pos:30px] to move the tail along its edge. */
:is(.sb-top,.sb-right,.sb-bottom,.sb-left){--sb-t:13px}
:is(.sb-top,.sb-right,.sb-bottom,.sb-left,.sb-tl,.sb-tr,.sb-bl,.sb-br)::after{content:"";position:absolute;width:0;height:0;border:var(--sb-t) solid transparent}
/* Tails on the four sides, centred by default */
.sb-top::after{bottom:100%;left:var(--sb-pos,50%);margin-left:calc(var(--sb-t) * -1);border-top-width:0;border-bottom-color:var(--sb-c)}
.sb-bottom::after{top:100%;left:var(--sb-pos,50%);margin-left:calc(var(--sb-t) * -1);border-bottom-width:0;border-top-color:var(--sb-c)}
.sb-left::after{right:100%;top:var(--sb-pos,50%);margin-top:calc(var(--sb-t) * -1);border-left-width:0;border-right-color:var(--sb-c)}
.sb-right::after{left:100%;top:var(--sb-pos,50%);margin-top:calc(var(--sb-t) * -1);border-right-width:0;border-left-color:var(--sb-c)}

/* Corner tails: the corner loses its radius and a right-angled triangle carries the edge on */
.sb-tl,.sb-tr,.sb-bl,.sb-br{--sb-t:14px}
.sb-tl::after{bottom:100%;left:0;border-width:0 var(--sb-t) var(--sb-t) 0;border-bottom-color:var(--sb-c)}
.sb-tr::after{bottom:100%;right:0;border-width:0 0 var(--sb-t) var(--sb-t);border-bottom-color:var(--sb-c)}
.sb-bl::after{top:100%;left:0;border-width:var(--sb-t) var(--sb-t) 0 0;border-top-color:var(--sb-c)}
.sb-br::after{top:100%;right:0;border-width:var(--sb-t) 0 0 var(--sb-t);border-top-color:var(--sb-c)}

/* Bordered bubbles: the tail is a rotated square with the same border on its two outer sides,
   painted over the bubble's own border so the outline runs unbroken into the point. */
.sb-line{--sb-line:var(--color-sb-ink);--sb-bw:2px;--sb-s:18px}
.sb-line::after{width:var(--sb-s);height:var(--sb-s);margin:0;border:0 solid var(--sb-line);background:var(--sb-c);translate:-50% -50%;rotate:45deg}
.sb-line.sb-top::after{top:0;bottom:auto;left:var(--sb-pos,50%);border-top-width:var(--sb-bw);border-left-width:var(--sb-bw)}
.sb-line.sb-bottom::after{top:100%;left:var(--sb-pos,50%);border-right-width:var(--sb-bw);border-bottom-width:var(--sb-bw)}
.sb-line.sb-left::after{left:0;right:auto;top:var(--sb-pos,50%);border-bottom-width:var(--sb-bw);border-left-width:var(--sb-bw)}
.sb-line.sb-right::after{left:100%;top:var(--sb-pos,50%);border-top-width:var(--sb-bw);border-right-width:var(--sb-bw)}

About this set

Twelve speech bubbles covering every tail position people ask for. Tails on four sides puts a centred triangle on the top, right, bottom or left edge of a filled bubble. Corner tails drops the radius on one corner and carries the two straight edges on into a right-angled point, the shape most comic and chat designs use. Bordered bubbles with matching tail borders is the version that usually goes wrong: an outlined box whose tail has exactly the same border, with no gap or seam where tail meets box. The filled tails are the classic border trick, a zero-size ::after with one coloured border. The outlined tails are a small rotated square with a border on its two outer sides, painted over the bubble’s own border so the outline runs unbroken into the point. Each bubble is one paragraph and a class such as .sb-top or .sb-br, the tail reads the bubble’s colour from –sb-c, and –sb-pos moves it along its edge, shown on the third outlined bubble. The drop shadow uses filter: drop-shadow, so it follows the tail as well as the box.

What’s included

  • Tails on the top, right, bottom and left edges
  • Four corner tails with the matching corner squared off
  • Outlined bubbles whose tail shares the border without a seam
  • --sb-c, --sb-t and --sb-pos set colour, tail size and tail position
  • Drop shadow that follows the tail

Accessibility

Each bubble is an ordinary paragraph, so screen readers read its text and nothing else; the tails are empty pseudo-elements and are not announced. Text colours reach at least 4.5:1 on every bubble colour, and the set contains no controls, so there is nothing to focus.

Customise it

Change --sb-c and --sb-ink on .sb for the bubble and text colour, --sb-t for the tail size and --sb-pos to slide the tail along its edge. Outlined bubbles add --sb-line, --sb-bw and --sb-s. In Tailwind set [--sb-c:…] next to bg-(--sb-c) and edit the --color-sb-* theme colours; Bootstrap uses the same variables with .p-3 and .rounded-4.