Creating comic book speech bubbles with SVG and javascript – The Humaan Blog


In September we launched ComicA11y, an experiment aiming to achieve an all-inclusive online comic reading experience. This meant tackling an age-old accessibility challenge — how to make visual elements work with assistive technologies.

The trickiest part of this experiment was how to accessibly render comic book dialogue, most commonly presented as text in speech bubbles. Webcomics have traditionally steered clear of this obstacle by making the text part of the image, but we wanted to explore whether it could be done using SVG and javascript.

Short answer: It CAN be done. And here is how we did it.

Anatomy of a speech bubble

First, let’s look at the speech bubble. It’s quite a simple shape: an oval with a trailing tail that points towards the speaking character. If you were to create this shape in Illustrator, it would take only a few seconds.

Two speech bubble shapes each outlined in black, showing their Bézier nodes in blue.

The geometry of a speech bubble. The example on the left shows the vector (Bézier) nodes of a speech bubble. The example on the right shows the nodes of the two simple shapes (an oval and a curved triangle) used to create that speech bubble.

Like speech bubbles in a standard webcomic, our accessible speech bubbles must be made up of unique sizes and curves relative to the art direction of the text. This is important for the final artwork to look right, so let’s start here…

An art directed paragraph using the line break element and CSS positioning. Even without visual curves, the text block appears circular (sort of). We will use this as a guide when using Javascript to draw SVG curves around the text, creating the final speech bubble.

CSS is the driving force for what is inside of the bubble. It configures the font-size, font-face and how much space will sit between the bubble and the text.

Speech bubble containing text that reads, "Check it out, I'm making a speech bubble". The text is enclosed within a pink rectangle enclosed in a larger pink rectangle, with arrows indicating how each element is positioned.

Blueprint for how CSS positioning will influence our speech bubble. The outlines and arrows indicate our designated elements and spacing. You can even add Comic Sans if you really want.

To create the bubble graphic, we’ll need to construct the SVG element using Javascript. Here’s a basic setup.

Nothing to look at yet, but we are dynamically measuring the width and height of the text. That information will be used to construct the SVG element in the next step.

Drawing the oval shape in SVG

Here’s where the fun starts. To construct our first shape, we’ll be using the in-built d attribute within the SVG path element, supplying it with a string containing the path information of the oval bubble.

To build this string, we need to break the entire path down into simple arc segments defined by cubic Bézier curves. We’ve chosen cubic Bézier curves as they give extra room around the corners — which comes in handy when dealing with text blocks that have more of a square shape (this happens quite often!). Here are our steps:

Steps for drawing the oval portion of the speech bubble using SVG’s d attribute. 1) Move to node 1. 2) Create Bézier curve from node 1 to node 2. 3) Continue curve to node 3. 4) Continue curve to node 4. 5) Continue curve to node 1.

6) Close off path.

Our final constructor will, of course, crunch the numbers for these steps dynamically, but for the sake of demonstration, let’s run through how the calculations work. In this example, our bubble has a width of 200px and a height of 100px.

1.) Move to node 1 – Starting in the top left on the SVG grid (x0, y0), we want to move to 0px on the x-axis and 50px to the middle of the y-axis (ie move straight down). The MoveTo (M) command requires only an x and y coordinate, so the first part of our d sequence will be d="M0,50".

2.) Create Bézier curve from node 1 to node 2 – A cubic Bézier curve requires 3 sets of x and y coordinates. The first set of coordinates indicate the position of the Out handle for the starting node (node 1). The second set of coordinates indicate the In handle for the destination node. The third set of coordinates gives us the destination node for the curve (node 2).

The three sets of x and y coordinates required to draw the first cubic Bézier curve.

Here’s what the d sequence looks like with the additional cubic Bézier curve information (C) to get from node 1 to node 2: d="M0,50 C0,20 50,0 100,0".

3.) Continue curve to node 3 – We can use the Smooth Bézier Curve command (S) to continue our curve. It requires only two sets of x and y coordinates as it’s designed to pick up where the last curve left off, reflecting the final handle from the previous node. So all we require is a set of coordinates for the control handle of this curve’s destination node, along with the coordinates for the destination node itself.

The sets of x and y coordinates required to continue the curve to node 3.

The updated d sequence with the additional smooth Bézier curve information will now be d="M0,50 C0,20 50,0 100,0 S200,20 200,50".

4.) Continue curve to node 4 – Continuing to use the Smooth Bézier Curve command, we can simply add the next pair of sets of x- and y- coordinates for first the destination control handle and then the set of x and y coordinates for the destination node.

Here’s the updated d sequence: d="M0,50 C0,20 50,0 100,0 S200,20 200,50 S180,100 100,100".

5.) Continue curve to node 1 – Now, we only require the last paired sets of x and y coordinates to get back to the original node. One last smooth Bézier curve should do it d="M0,50 C0,20 50,0 100,0 S200,20 200,50 S180,100 100,100 S0,80 0,50".

6.) Close off path – Finally, we complete the path using the ClosePath action (z): d="M0,50 C0,20 50,0 100,0 S200,20 200,50 S180,100 100,100 S0,80 0,50 z".

The completed path we get from following the steps, dynamically generated using Javascript.

Interlude: Setting up the tail shape.

Let’s suppose that this particular tail will appear as so:

Speech bubble outlined in black. The tail is short, anchored to the bottom right side of the oval bubble, pointing downwards and curving inwards.

The tail is short, anchored to the bottom right side of the bubble, pointing downwards and curving inwards.

Because the SVG is constructed with two individual shapes overlaid on top of each other, it’s tricky to get the tail to sit precisely along the curve of the oval. Thankfully here’s some maths that solves this problem for us.

This Bezier function returns the x and y coordinates of a fixed point along a cubic Bézier curve.

When given the coordinates for a cubic Bézier curve (control handles and nodes), the function will return x and y coordinates of a point along the line from a scale of 0 – 1 where 0 is the start of the curve and 1 is the end.

Using the Bézier function, we’re able to input the Bézier curve information and output coordinates along the curve.

Now, here’s a simple setup showing the Bezier function in action.

Using the Bezier function along with node and handle information from the oval path, we can return x and y coordinates for nodes running along the Bézier curve at points .2 and .45 on the curve.

Then, we set up the tail shape by adding it to the SVG constructor, similar to how we set up the oval shape. The code below shows an isolated view of what we’ll need for the tail minus the path information (t_path). We’ll be adding all that in the section, Drawing the tail shape.

The additional setup needed for the new path, minus the d attribute.

Lastly let’s suppose our tail shape has a height of 25 pixels in addition to the oval shape, taking the SVG canvas size to 125px. We’ll need to make a small alteration to our SVG attribute settings to allow for a tail:

Additional t_length added to b_height added to allow for a tail shape.

And now we’re ready to draw the tail.

Drawing the tail shape

Like with the oval shape, we supply the path element’s d attribute with commands for drawing the necessary lines and curves to create a tail.

Steps for drawing the speech bubble tail using SVG’s d attribute. 1) Move to node 1. 2) Create a horizontal line to node 2. 3) Create a bezier curve to node 3. 4) Create a vertical line to node 1.

5) Close off path.

1.) Move to node 1 – Starting in the top left on the SVG grid (x0, y0) we want to move to the x and y coordinates returned from the Bezier function in our code. The MoveTo command (M) requires only an x and y coordinate, so the first part of our d sequence will be d="M180,80".

2.) Create a horizontal line to node 2 – Using the lineTo command (L), we can provide destination x- and y-coordinates to draw a straight line between origin and destination. The destination x and y coordinates are also returned using our Bezier function. Here’s the updated  d sequence: d="M180,80 L150,90".

3.) Create a Bézier curve to node 3 – The QuadraticBézierCurves command (Q) only requires 3 points, unlike the CubicBézier command (C) that requires 4. So, we only need to provide the x and y coordinates for the control handle and the destination node: d="M180,80 L150,90 Q180,80 180,125".

4.) Create a vertical line to node 1 – We’re going to use the lineTo command (L) once again to draw one more line, returning to node 1. Here’s the updated d sequence: d="M180,80 L150,90 Q180,80 180,125 L180,80".

5.) Close off path – Finally, we use the ClosePath action (z) to end the sequence: d"=M180,80 L150,90 Q180,80 180,125 L180,80 z".

The updated SVG with oval and tail shape.

We’re almost done. Because the oval and tail shapes sit on top of each other on the z-axis, their strokes (outlines) overlap, making it look like the tail is separate to the bubble — it is separate, but we don’t want it to look that way!

So, as a finishing step we’re going to duplicate the tail and position the oval between them.

The layers that make up the bubble SVG.

The tail that sits on top will have no stroke, only a white fill. The tail that sits behind will have a double-thick stroke and black fill. Overall, this trick will give the illusion of a single shape with a black outline.

The completed bubble SVG, appearing as a single shape with a continuous outline.

We used this method to create all of the speech bubbles in the ComicA11y experiment albeit with more customisation options like text sizing, content translation, and support for both LTR and RTL reading.

Solving this problem was a great challenge and an exciting demonstration of how something I’m passionate about can be enhanced for digital-first creators and audiences. I learned so much along the way. Hope you did too!