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.
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.
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…
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.
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:
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
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).
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.
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="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".
Interlude: Setting up the tail shape.
Let’s suppose that this particular tail will appear as so:
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.
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.
Now, here’s a simple setup showing the
Bezier function in action.
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.
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:
And now we’re ready to draw the tail.
Drawing the tail shape
Like with the oval shape, we supply the
d attribute with commands for drawing the necessary lines and curves to create a tail.
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
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
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="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".
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 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.
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!