A Bubble is a Popover drawn as a speech bubble. It is placed the
same way and supports the same data-ddi-align values; the only difference is
its chrome — a rounded body with a tail that points at the centre of the trigger,
on the side facing it.
The chrome is an SVG rendered behind the content by the ddi-speech-bubble
custom element, which takes its box and its corner radii from the bubble’s own
container. So set the shape with the usual utility classes (rounded,
rounded:l, round) and it follows.
Two things differ from a plain Popover:
- Don’t add
bg— the SVG paints the background. Use--ddi-bubble-fillto change its colour. - Don’t add a CSS
border— it would draw a rectangle around the bubble. Use--ddi-bubble-strokeinstead, which traces the body and the tail as one line.
drop-shadow works as you’d expect: the shadow follows the bubble’s silhouette,
tail included.
A Bubble is placed exactly like a Popover: it has an id, and any element with a data-ddi-popovertarget attribute set to that id toggles it. The tail points at the centre of the trigger.
---
import { Bubble, Text } from "@dynamic-type/ddi/server";
---
<Text class="p:airy">
<p>
A <code>Bubble</code> is placed exactly like a <code>Popover</code>: it
has an id, and any element with a <code>data-ddi-popovertarget</code> attribute
set to that id toggles it. The tail points at the centre of the trigger.
</p>
{(<button data-ddi-popovertarget="example-bubble">Hit me!</button>)}
</Text>
<Bubble id="example-bubble" class="rounded drop-shadow p">
I am a bubble
</Bubble>data-ddi-align works the same as on a Popover. With top-center the bubble opens below the trigger, and the tail sits on its top edge.
---
import { Bubble, Text } from "@dynamic-type/ddi/server";
---
<Text class="p:airy">
<p>
<code>data-ddi-align</code> works the same as on a <code>Popover</code>.
With <code>top-center</code> the bubble opens below the trigger, and the tail
sits on its top edge.
</p>
{
(
<button
data-ddi-popovertarget="example-bubble-2"
data-ddi-align="top-center"
>
Hit me!
</button>
)
}
</Text>
<Bubble id="example-bubble-2" class="rounded drop-shadow p">
I am a bubble
</Bubble>bottom-center opens the bubble above the trigger, so the tail flips to the bottom edge — the side facing the trigger.
---
import { Bubble, Text } from "@dynamic-type/ddi/server";
---
<Text class="p:airy">
<p>
<code>bottom-center</code> opens the bubble above the trigger, so the tail
flips to the bottom edge — the side facing the trigger.
</p>
{
(
<button
data-ddi-popovertarget="example-bubble-3"
data-ddi-align="bottom-center"
>
Hit me!
</button>
)
}
</Text>
<Bubble id="example-bubble-3" class="rounded drop-shadow p">
I am a bubble
</Bubble>Styling
Every option is an inheritable CSS custom property, so one declaration can style a whole region of the page.
| Property | Default | Effect |
|---|---|---|
--ddi-bubble-fill |
var(--ddi-c-bg) |
Fill of the body and the tail. |
--ddi-bubble-stroke |
none |
Outline colour. Unset means no outline at all. |
--ddi-bubble-stroke-width |
1px |
Only visible once --ddi-bubble-stroke is set. |
--ddi-bubble-tail-size |
0.75rem |
How far the tail reaches past the bubble’s edge. |
--ddi-bubble-tail-width |
1.25rem |
Width of the tail’s base along the edge. |
--ddi-bubble-tail-gap |
0.25rem |
Clear space left between the tail tip and target. |
--ddi-bubble-countdown |
var(--ddi-c-accent) |
Colour the countdown outline starts at (see below). |
--ddi-bubble-countdown-width |
2px |
Thickness of that outline. |
ddi-speech-bubble pushes the bubble tail-size + tail-gap away from the
trigger and draws the tail into the room it just made, so the tip stops just
short of the trigger instead of running into it.
Fill, outline and tail size are custom properties. They inherit, so set them on the bubble itself or on any ancestor. No outline is drawn until--ddi-bubble-stroke is given a colour.
---
import { Bubble, Text } from "@dynamic-type/ddi/server";
---
<Text class="p:airy">
<p>
Fill, outline and tail size are custom properties. They inherit, so set
them on the bubble itself or on any ancestor. No outline is drawn until
<code>--ddi-bubble-stroke</code> is given a colour.
</p>
{
(
<button
data-ddi-popovertarget="example-bubble-4"
data-ddi-align="top-center"
>
Hit me!
</button>
)
}
</Text>
<Bubble
id="example-bubble-4"
class="rounded:l p"
style="--ddi-bubble-fill: var(--ddi-c-bg-alt);
--ddi-bubble-stroke: var(--ddi-c-accent);
--ddi-bubble-stroke-width: 2px;
--ddi-bubble-tail-size: 1.25rem;
--ddi-bubble-tail-width: 2rem;
--ddi-bubble-tail-gap: 0.5rem"
>
I am a bubble
</Bubble>Behavior
The behavior prop says what a bubble does once it is open, on top of the
dismissal every popover already has — a click elsewhere, or a scroll, closes it.
It defaults to "none", which adds nothing.
The other two both close the bubble on their own, and are two ways of saying so:
"timed-success-info" draws the time left as an outline, "autofade-success-info"
lets a slow fade carry it. Pick one per surface rather than mixing them. Either
way, hovering or focusing the bubble — at any point, the fade included — stops it
for good.
"timed-success-info"
"timed-success-info" is for a confirmation that has served its purpose after a
glance: it fades itself away after three seconds. Hovering it — or moving focus into it
— cancels that countdown for good, on the assumption that a reader who has
gone to the bubble is still reading it. Moving the pointer away again doesn’t
restart it, so from then on it waits to be dismissed like any other bubble.
Reopening the bubble starts a fresh countdown.
None of that is any use if the bubble looks like it just vanished, so the countdown is shown as well as run: the bubble’s own outline unwinds over the three seconds, so the time left is on screen the whole way. Because the outline is a second copy of the silhouette the chrome already draws, it fits whatever shape you gave the bubble, tail included.
It drains from the tip of the tail, clockwise — the tail is the part aimed at
whatever the reader just did, so it is where the eye already is. That is why
bubblePath starts a tailed outline at the tip: where a closed path begins is
invisible to fill and stroke, but it is where a dash along it starts.
The outline also cools as it goes, from --ddi-bubble-countdown to the bubble’s
own --ddi-bubble-fill, so what is left of it has settled into the chrome by the
time it runs out rather than staying loud to the last pixel. And the bubble
doesn’t blink out at the end — it fades, which is what makes the closing read
as the bubble taking itself away rather than as something you missed. Reaching it
during that fade, by pointer or by focus, pulls it back and pins it.
Set --ddi-bubble-stroke too and the two compose the way a progress indicator
normally does: the static outline is the track, the countdown is the fill.
Cancelling fades the countdown out where it stopped, which is what tells a reader
their hover took effect. Under prefers-reduced-motion: reduce the outline
retreats in three once-a-second steps instead of sliding.
behavior="timed-success-info" is for a confirmation you only need to glance at: it fades itself away after three seconds, and its own outline unwinds from the tip of the tail — cooling into the bubble's fill as it goes — to show how long is left. Hover or focus it, even during the fade, and it stops for good: the outline fades out and from then on the bubble waits to be dismissed like any other.
---
import { Bubble, Text } from "@dynamic-type/ddi/server";
---
<Text class="p:airy">
<p>
<code>behavior="timed-success-info"</code> is for a confirmation you only
need to glance at: it fades itself away after three seconds, and its own outline
unwinds from the tip of the tail — cooling into the bubble's fill as it goes
— to show how long is left. Hover or focus it, even during the fade, and it
stops for good: the outline fades out and from then on the bubble waits to
be dismissed like any other.
</p>
{
(
<button
data-ddi-popovertarget="example-bubble-5"
data-ddi-align="top-center"
>
Save
</button>
)
}
</Text>
<Bubble
id="example-bubble-5"
class="rounded drop-shadow p"
behavior="timed-success-info"
style="--ddi-bubble-stroke: var(--ddi-c-border-light)"
>
Saved!
</Bubble>"autofade-success-info"
The same contract told a different way. It holds at full strength for two seconds — less than the timed variant, because what follows is still readable — then takes two more to fade away. Nothing counts down and no outline is drawn: the fade is slow enough to be the indicator by itself, which suits a bubble you’d rather not put a second moving part on.
That length is also what makes it forgiving. A reader who notices the bubble going has the whole fade to reach it, and doing so brings it back to full strength and pins it, exactly as hovering a countdown does.
behavior="autofade-success-info" says the same thing without drawing anything extra: it holds for two seconds, then spends two more slowly fading out. There is no countdown outline — the fade is long enough to be the indicator by itself. Catch it at any point, pointer or focus, and it comes back to stay.
---
import { Bubble, Text } from "@dynamic-type/ddi/server";
---
<Text class="p:airy">
<p>
<code>behavior="autofade-success-info"</code> says the same thing without
drawing anything extra: it holds for two seconds, then spends two more slowly
fading out. There is no countdown outline — the fade is long enough to be
the indicator by itself. Catch it at any point, pointer or focus, and it comes
back to stay.
</p>
{
(
<button
data-ddi-popovertarget="example-bubble-6"
data-ddi-align="top-center"
>
Save
</button>
)
}
</Text>
<Bubble
id="example-bubble-6"
class="rounded drop-shadow p"
behavior="autofade-success-info"
>
Saved!
</Bubble>Either self-closing bubble carries role="status", so the message is announced
when it appears — neither can be relied on to still be there when a screen-reader
user goes looking for it.
Notes
- The tail is centred on the trigger, but never crosses a corner arc: near a
corner it slides along the edge, and on an edge too short to hold it, it
narrows. A fully rounded bubble (
round) has no straight edge to attach to, so it gets no tail. - Fine-tuning the tail’s position along the edge isn’t supported yet.
- Like any popover, a bubble closes on scroll and resize, so nothing needs to redraw it while it is open.