Annotations Tutorial: Speech bubbles

In this tutorial on per-paragraph commenting, we will design the collapsed annotations view. We will design and evaluate three different types of speech bubbles using only CSS3 techniques and see which one suits our requirements best.


In the previous tutorial, we created the foundations of our page on which annotations app is going to work. We have some part of our markup ready and would be taking over where the last tutorial left off. Now, we'd want to add more markup - the annotations! But before we went ahead and added them up, we'd like to see how they would look when folded. I imagine there should be some little comment bubble on the margin where we want to show that an annotation has been made, or can be made.

Different types of speech bubbles(Image Credits)

Reading from left to right, we'd most likely encounter it on the right hand side. Further, we choose a paragraph as our quanta of commenting, that is, these annotation bubbles will be found in a position relative to the paragraph, not linewise. This is mainly because a <p> is our textual block. Too many spans are causes of severe headaches and a messy soup.

So, we'll place these comment blocks to the right of our article, and will hang them near the top-right edge. Why? Why top? Why not bottom? Well, we can. Okay, chuck it, we will put it in the bottom edge, but on the right side, for now.

That should be simple. We will:

  • add another block element,
  • make it relatively positioned,
  • and float the bubble to its right, or probably, fix it to the right.

Now, how do we make a bubble?

We would want two states in the bubble:

  1. When no annotations have been made, it'd show a '+' sign, which means, add a comment.
  2. When we have some public annotations, it would show the number of annotations on that content block.

Hmm, so, does a circular bubble fit the bill? It has to be large enough to house numbers, but small enough not to be a thorn in the eyes of the readers. We could make each kind of bubble and see which one looks better.

So, lets create:

  1. An oval bubble
  2. A rectangular bubble

We'll use this simple CSS3 construction by Nicolas Gallagher for these speech bubbles as inspiration.

First, lets make the central oval part:

.oval-speech {
position:relative;
min-width: 25px;
min-height: 20px;
width:auto;
padding: 0;
margin: 0;
text-align:center;
color:#fff;
background:#5a8f00;
/* css3 */
@include background-gradient(#b8db29, #5a8f00);
@include border-radius(100%, 100%);
}

Where, our background-gradient mixin looks like:

@mixin background-gradient($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%){
background:-webkit-gradient(linear, 0 $start-percent , 0 $end-percent, from( $start-color ), to( $end-color ));
background:-moz-linear-gradient($start-color, $end-color);
background:-o-linear-gradient($start-color, $end-color);
background:linear-gradient($start-color, $end-color);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=1); // IE9 and down
}

and border-radius like:

@mixin border-radius($x-radius: 0px, $y-radius: 0px ) {
-webkit-border-top-left-radius:$x-radius $y-radius;
-webkit-border-top-right-radius:$x-radius $y-radius;
-webkit-border-bottom-right-radius:$x-radius $y-radius;
-webkit-border-bottom-left-radius:$x-radius $y-radius;
//Escape 
-moz-border-radius:#{$x-radius}/#{$y-radius};
-ms-border-radius:#{$x-radius}/#{$y-radius};
border-radius:#{$x-radius}/#{$y-radius};
}

The curvature of the edges into the form of an oval is achieved by the border radius property, which we've set to 100% in x and 100% in the y direction. Now, because the width of the elements is usually going to be larger than their heights, it is unlikely that the shape will resemble a circle, or worse, a standing oval (with width smaller than height). For the just in case scenario, we've put a minimum limit on the width and height.

HTML and CSS rendering is done like a painter's canvas model. What comes before is like a layer of colour, and the one that comes after it is another layer drawn on the previous one.

Now comes the extended part of the speech bubble which is the actual fun part. Drawing the elephant is relatively easy, but it is quite hard to draw its tail. We will use pseudoselectors to achieve that. To do so, we need to realize that HTML and CSS rendering is done like a painter's canvas model. What comes before is like one layer of colour, and the one that comes after it is another layer drawn on top of the previous one. So, if the previous one was painted blue, and the new one is yellow, the overlapping parts would look green.

Second, what are we going to do with pseudo selectors? We know that we can curve the borders of a content block or DOM object. Also, this article visually explains how the borer comprises of 4 regions which can together be used to create some shapes.

Things can get tricky on the small hairline shape, so we'll first make bigger ones, to see this pseudoselector method in action. I suggest that you try this out as we go on, you could play with it in the codepen shown below.

See the Pen WbBQJY by Anshul Thakur (@anshulthakur) on CodePen.

<div class="solid-block"></div>
.solid-block{
height: 0px;
width: 0px;
position: relative;
margin-bottom: 100px;
}

No content, an empty block, but magic happens outside it. Add some SASS within .solid-block :

&:before{
content:"";
position: absolute;
bottom: -60px;
right:0;
border: 60px solid black;
border-top: 0px solid transparent;
border-left: 0px solid transparent;
}
&:after{
content:"";
position: absolute;
bottom: -60px;
right:0;
border: 60px solid gray;
border-top: 0px solid transparent;
border-left: 0px solid transparent;
transform: translate(-50%,0%);
}

The '&' sign is an alias for the current container. In our case, & will be replaced by solid-block . Compile the sass. (Or as I like it, I've put my files on watch and sass automatically compiles them on detecting any change).

What do we have? Two 60 pixel high blocks of gray and black colours huddled together where we had nothing moments before.

The :before pseudo selector created a block with no content ( content:"" ) but with a right and bottom border of 60px width and black color. Now, since there is no content, this would get us a box of black color below the line with its width as 60px in both right side, and in the bottom. When I say width, it is the x-axis width for the right sided border, and y-axis width in the bottom border, so don't get confused that the bottom looks longer. The :after pseudo selector created a similar block, and because of the transform property, it has been moved 50% of its initial position towards the negative x-axis, thus revealing some part of the underlying black block.

Now, we want it to curve. So, let's add some border radius to the :before property:

border-bottom-right-radius: 100% 100%;

Reloading the page (or if you are working on the codepen, it would autorefresh) shows that the lower edge of the black block has curved. Now, lets curve the gray block too, by adding the following to :after property:

border-bottom-right-radius: 100% 100%;

Alright, if we can just make the gray portion disappear along with whatever it was masking, the section that remains would look like the squiggle under the speech bubble. But it isn't pointed. Hmm, I don't quite get it! I have carefully slid the gray box over the black one and both are exactly identical, then there must be a pointed tip on the lower end. As a workaround, let's increase the y-radius of the gray block.

border-bottom-right-radius: 100% 150%;

That does it. There we have, a tiny squiggle which can be place below our chat bubble. Thus, our speech bubble stylesheet will now look like:

.oval-speech {
position:relative;
min-width: 25px;
min-height: 20px;
width:auto;
padding: 0;
margin: 0;
text-align:center;
color:#fff;
background:#5a8f00;
/* css3 */
@include background-gradient(#b8db29, #5a8f00);
@include border-radius(100%, 100%);
p {
font-size:0.7em;
margin:0;
padding:0 5px;  
}
/* creates part of the curve */
&:before {
content:"";
position:absolute;
z-index:-1;
bottom:-10px;
right:50%;
background:#5a8f00; /* need this for webkit - bug in handling of border-radius */
border: 10px solid #5a8f00;
border-top: 0px solid transparent;
border-left: 0px solid transparent;
/* css3 */
@include border-bottom-right-radius(100%,100%);
/* using translate to avoid undesired appearance in CSS2.1-capabable but CSS3-incapable browsers */
@include transform-translate(0, -2px);
}
/* creates part of the curved pointy bit */
&:after {
content:"";
position:absolute;
z-index:-1;
bottom:-10px;
right:50%;
background:#fff;
border: 10px solid white;
border-top: 0px solid transparent;
border-left: 0px solid transparent;
/* css3 */
@include border-bottom-right-radius(100%,150%);
/* using translate to avoid undesired appearance in CSS2.1-capabable but CSS3-incapable browsers */
@include transform-translate(-50%, -2px);
}
}

I'm sure you could make the remaining mixins in a fashion I made for border-radius. Try filling in large values like ' 154 ', or something and notice how the bubble grows proportionately. The complete pen can be seen here.

There is one caveat in this technique. Note that we set the color of the :after element to white; well, if the background color isn't white, it will hurt like a sore thumb sticking out of wet socks. That is where I like SASS. We can set a variable for background-color , and then the change would have to be made only to the variable, and everywhere it was used, the values will be reflected.

Now, let us have a look at the rectangular bubble too, before we can make our mind on which one to use.

Just changing the radius of the main bubble makes it rectangular, but then, the squiggle doesn't look too good, does it? Lets try a plain triangle.

.rectangular-speech{
position:relative;
min-width: 25px;
min-height: 20px;
width:auto;
padding: 0;
margin: 0;
text-align:center;
color:#fff;
background:#5a8f00;
/* css3 */
@include background-gradient(#b8db29, #5a8f00);
@include border-radius(15%, 15%);
p {
font-size:0.5em;
margin:0;
padding:0 3px;  
}
/* creates part of the curve */
&:before {
content:"";
position:absolute;
z-index:-1;
bottom:-10px;
right:30%;
border: 10px solid #5a8f00;
border-right: 10px solid transparent;
border-bottom: 10px solid transparent;
/* css3 */
/* using translate to avoid undesired appearance in CSS2.1-capabable but CSS3-incapable browsers */
@include transform-translate(0, -2px);
}
}

Now, that we have both, which one looks better? I asked my buddy sitting next to me and he said the rectangular one. I had liked the rectangular one the moment I had seen one, my mind did a double somersault. So, by a vote of 2-0, we have a winner! But, should you want the other one, you know, it is just a class-name change away.

Okay, in the last leg of this tutorial, let us make them less obtrusive. Currently, they look too bright to be not-noticed even if we don't want to see them. So, here's a new constraint on them. If no annotations have been made thus far on an object, the bubble must not be visible. Also, the bubble with ' + ' sign must be visible slightly only when we hover on the respective content block. If there have been annotations, they will only be slightly visible and only when we hover on them will they get full color.

Sounds right? Let's begin.

First, hide all of them. We will introduce another CSS class name rather than modifying each and every speech shape; by doing this, we ensure that all speech bubbles get that effect, regardless of their shape. Let's call it speech .

.speech{
display: none;
}

Reload the page, and all bubbles are now gone!

Now, we want to show only those which DO NOT have a ' + ' as content. Now, we have a problem there. There was a contains() property suggested, but did not make it into the CSS3 specs. So, we need more manipulation. We can:

  1. Put a specific class to flag that we have annotations or to mark its absense.
  2. Or we could add a data-xxx attribute.

For simplicity sake, we'll do the first. We'll make the blocks with annotations with ' has-annotations ' class. With this, we can do away with the ' speech ' class too. If it has comments, i.e. ' has-comments ' class, we'll hide it.

&.has-annotations{
display:none;
}

This hides what we wanted to hide. Now, lets change the behaviour of hovering on the ' + ' element. Also, we don't want all of them to look so bright at the outset, but only on hover. So, we can change the default background color to something light. Also, we were setting its background-gradient, so we need to remove that too.

background-color: rgba(228, 226, 226, 0.8);
@include background-gradient(#dcdada, #dcdada);

But wait! The squiggle looks bad. What we want is, we set the color only once and it applies to all of them, right? Right! The squiggles are essentially borders, and we haven't specified the border colors of our parent element (into which the :before and :after will be added to). So, what we can do is, set its color property to the value we set in background-color, and let its descendants inherit that.

border-color: rgba(228, 226, 226, 0.8);

Now, it looks fine. So far so good. Now, we want that whenever we hover on top of it, it becomes green. That is done with the :hover property.

&:hover{
background-color: rgba(184, 219, 41, 0.8);
@include background-gradient(#b8db29, #5a8f00);
border-color: rgba(110, 131, 24, 0.8);
}

Looking nice? Sort of. Okay, so, one more thing is left. The ones with annotation count must be visible, and the ones without mustn't, until we hover on top of the content block. So, we need to invert our logic. The has-annotations class is by default, visible, and the one without isn't. That is easy enough for you to do, just set display:block for .has-annotations and none in the default block.

One last effect is to display the ' + ' (add annotation) block when we hover on its parent content object. Currently, there is no simple way to just tell who the preceding element was. Instead, what we can do is to set the chain rule on every content block that is capable of having annotations. If you notice in the markup, the ones which can have annotations have some properties: first, they belong to a parent with ID 'commentable-container', and second, they themselves have a unique ID. So, we can make a rule that the immediate sibling of any content block which is a child of #commentable-container must become visible when we hover on the block.

#commentable-container{
& *[id]:hover + .comments > .comments--toggle, 
& *[id] + .comments:hover > .comments--toggle {
display: block;
}
}

The completed page looks like the pen below

See the Pen yyWYow by Anshul Thakur (@anshulthakur) on CodePen.

This has two immediate issues, one, the hovered element disappears as soon as we hover out. This means we cannot click on it, for the moment we move towards it and hover out of the content block, it disappears (except when we directly transition into it from the top).

Second, on a mobile screen with touch, there is no hover effect, well, because we don't have a mouse there. For now, the invisibility and hover effects will have to go. So, we have to think of something simpler than this hokey-pokey. But that we'll do in the next segment.

This completes our second pass over the layout. We started with a vertically stacked, no-bling kind of content that would work suitably over small as well as big screens. Then, we imagined how and where our annotation icons would have to lie in order to be more obvious and likeable. No annotations have been made thus far, just a symbol to signify that there are some made already, or can be made.

In the next tutorial, we will visualize how the annotations must look like in a rolled out state