DDI

Bubble

This page was updated in v2.22.0

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:

drop-shadow works as you’d expect: the shadow follows the bubble’s silhouette, tail included.

A bubble opens below its trigger, tail pointing at it

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.

I am a bubble
---
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.

I am a bubble
---
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.

I am a bubble
---
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.

I am a bubble
---
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.

Saved!
---
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.

Saved!
---
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