← back to articles

Building a Jekyll Site – Part 3 of 3: Creating a Firebase-Backed Commenting System | CSS-Tricks

Save article ToRead Archive Delete · Log out

37 min read · View original · css-tricks.com

The following is a guest post by Mike Neumegen from CloudCannon. This final post is about adding some functionality to a Jekyll site that isn't possible: comments. That's because Jekyll has no backend component in which to save comments. But, we don't even need that if we do it entirely front-end with Firebase!

This is a three-part series:

Part 1: Converting a Static Website To Jekyll
Part 2: Adding a Jekyll CMS with CloudCannon
Part 3: (This post) Creating a Firebase-Backed Commenting System

In this series, we're building a site with a blog and content management system for Coffee Cafe, a fictional cafe. This final post is about building a custom commenting system with Firebase.

Custom built solutions provide more control of the design, functionality and data than drop-in solutions, such as Disqus and Facebook Comments.

What is Firebase?

Firebase is a real-time, scalable backend. It allows developers to build applications with authentication and persistent data for static websites.

We're going to store our blog comments in Firebase and retrieve them when someone views a blog post.

Sign Up

First, sign up for a Firebase account.

Once you've signed up, create a new app for the blog comments and record the App URL for later.

Setup

We need a number of JavaScript libraries to run the commenting system. Firebase saves and fetches comments, jQuery adds elements to the page, Moment formats dates, and blueimp-md5 generates MD5s. `/js/blog.js` contains the custom application code for the commenting system

Add the following scripts above </body> in `_layouts/default.html` (or do whatever build process / concatenation thing you normally do):

<script src="https://cdn.firebase.com/js/client/2.2.1/firebase.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.1.0/js/md5.js"></script>
<script src="/js/blog.js"></script>

Firebase Overview

When a visitor views a blog post we get all the relevant comments from Firebase.

Visitors post comments with a name, email address and message. We take this information, add a timestamp and the current page, then store it in Firebase.

In Firebase, data is stored as JSON objects. The comments are stored as an array of objects for each blog post:

{
  "/tutorial/2016/01/02/title-tag.html": [
    {
      "name": "Bill",
      "email": "bill@example.org",
      "message": "Hi there, nice blog!",
      "timestamp": 1452042357209
    },
    {
      "name": "Bob",
      "email": "bob@example.org",
      "message": "Wow look at this blog.",
      "timestamp": 145204235846
    }
  ],
  "/announcement/2016/01/01/latest-seo-trends.html": [
    {
      "name": "Steve",
      "email": "steve@example.org",
      "message": "First post!",
      "timestamp": 1452043267245
    }
  ]
}

Implementation

Firebase references provide read and write access to the database. Add a reference to the database in `/js/blog.js`:

var ref = new Firebase("https://<YOUR-APP-ID>.firebaseio.com/");

ref gives us access to the root of the database. We can get a reference to a blog post using ref.child("<PATH_TO_BLOG_POST>").

Saving Comments

The path is a great way to identify a blog post, but Firebase doesn't support characters like ampersands in the key name. To solve this issue, add a function to replace unsupported characters:

function slugify(text) {
  return text.toString().toLowerCase().trim()
    .replace(/&/g, '-and-')
    .replace(/[\s\W-]+/g, '-')
    .replace(/[^a-zA-Z0-9-_]+/g,'');
}

Save a reference to the slugified current path:

var postRef = ref.child(slugify(window.location.pathname));

Add a form to post new comments below the blog posts. Enter the following markup below

background-clip is one of those properties I've known about for years, but rarely used. Maybe just a couple of times as part of a solution to a Stack Overflow question. Until last year, when I started creating my huge collection of sliders. Some of the designs I chose to reproduce were a bit more complex and I only had one element available per slider, which happened to be an input element, meaning that I couldn't even use pseudo-elements on it. Even though that does work in certain browsers, the fact that it works is actually a bug and I didn't want to rely on that. All this meant I ended up using backgrounds, borders, and shadows a lot. I also learned a lot from doing that and this article shares some of those lessons.

Before anything else, let's see what background-clip is and what it does.

In the following image, we have an element's box model.


the box model

If the padding is 0, then the padding-box is exactly the same size as the content-box, and the content limit coincides with the padding limit.


with padding: 0

If the border-width is 0, the border-box is the same size as the padding-box, and the border limit coincides with the padding limit.


with border-width: 0

If both the padding and the border-width are 0, then all the three boxes (the content-box, the padding-box, and the border-box) have the same size, and the content limit, the padding limit, and the border limit all coincide.


with padding: 0 and border-width: 0

By default, backgrounds cover the entire border-box (they are applied underneath the border as well), but their background-position (and also %-based background-size) is relative to the padding-box.

In order to better understand this, let's consider an example. We take a box with random dimensions, give it a simple gradient background of background-size: 50% 50% and a hashed border (using border-image) so we can still see through the hashes what's underneath the border:

In this demo, we can see that the gradient background covers the entire border-box (it's visible underneath the hashed border). We haven't specified a background-position, so it takes the default value — 0 0. We can see this is relative to the padding-box because it starts from the top left corner (the 0 0) of this box. We can also see that the background-size set in % is relative to the padding-box.


by default, backgrounds cover the entire border-box, but start from the top left corner of the padding-box

When setting the background-size for a gradient (but not for actual images), we usually need two values for consistent results across browsers. If we use just one value, Firefox takes the second one to be 100% (per spec), while every other browser incorrectly takes the second value to be equal to the first. A missing background-size value is taken to be auto and since gradients have no intrinsic dimensions or intrinsic proportions, the auto value cannot be resolved from those, so it should get treated as 100%. So unless we want both dimensions of our background-size to be 100%, we should use two values.


Using just one value for setting the background-size doesn't produce consistent results across browsers (live test); left: Firefox (per spec, takes the second value to be 100%); right: Chrome/ Opera, Safari, IE/ Edge (incorrectly take the second value to be equal to the first)

We can make the background cover just the padding-box or just the content-box with the help of background-clip. Clipping means cutting out and not displaying what falls outside the clipping region, where the clipping region is the area inside the dotted line in the illustration below.


illustration of what clipping is

In the default case of background-clip: border-box, the clipping region is the border-box, so we have the background underneath the border as well.


background-clip: border-box

If we set background-clip: padding-box, the clipping region is the padding-box, meaning that the background is only displayed within the padding-box limit (it doesn't go underneath the border).


background-clip: padding-box

And finally, if we have background-clip: content-box, the clipping region is the content-box, so the background is only displayed within the content-box limit.


background-clip: content-box

These three situations are illustrated by the following live demo:

We also have another property called background-origin that specifies which of the three boxes the background-position (and background-size, if expressed in %) is relative to.

Let's consider that we have an element like before, with a hashed border and, this time, a visible padding. We layer an actual image and a gradient on the background. Both have background-size: 50% 50% and are not repeating. In addition to this, the image has background-position: 100% 100% (we leave the default 0 0 for the gradient):

background: linear-gradient(to right bottom, 
      #e18728, #be4c39, #9351a6, #4472b9), 
  url(tiger_lily.jpg) 100% 100%;
background-repeat: no-repeat;
background-size: 50% 50%;

The following demo illustrates what happens for each of the three possible values for background-originborder-box, padding-box, and content-box:

The 100% 100% specified by the background-position of the actual image is the 100% 100% of the box specified by background-origin. At the same time, the 50% 50% specified by the background-size means half the width and half the height of the box specified by background-origin.

In the background shorthand, background-origin and background-clip can be specified in this order at the end of the layer. Since they both take a box value, if just one box value is specified, then both are set to it. If two box values are specified, background-origin is set to the first and background-clip is set to the second. If no box values are specified, they just take the default values (padding-box for background-origin and border-box for background-clip).

All right! Now let's see how we can use this to our advantage!

Transparent gap between border and background

Some may remember we can get semitransparent borders with background-clip. But, we can also introduce a space between the border and the area with a background without introducing an extra element. The simplest way to do this is to have a padding in addition to a border, and also set background-clip to content-box. By doing it in the shorthand with just one box value, we're also setting background-origin to content-box, but that's fine in this case, it doesn't have any unwanted effect.

border: solid .5em #be4c39;
padding: .5em;
background: #e18728 content-box;

Clipping the background to content-box means it doesn't extend beyond the content limit. Beyond that, we have no background, so we can see what's underneath our element. Adding a border means we see that border between the padding limit and the border limit. But, if the padding is non-zero, we still have a transparent area between the content limit and the padding limit.


highlighting the border, padding, and content area via dev tools

We can test it live with this Pen:

We can make things more interesting by adding a drop-shadow() filter that gives the whole thing a yellowish glow:

Prefix reminder: I've seen a lot of resources adding -moz- and -ms- prefixes for CSS filters. Please, don't do that! CSS filters have been unprefixed in Firefox ever since they were first implemented (Firefox 34, autumn of 2014) and now they've landed in Edge behind a flag - also unprefixed! So CSS filters never needed the -moz- or -ms- prefixes, it's completely useless to add them, the only thing they do is bloat the stylesheet.

We can also get a cool looking effect if we use a gradient for both the background-image and the border-image. We'll make a gradient that starts from a solid orange/red at the top, then fades down to complete transparency. Since only the shades are different and otherwise the gradients used are identical, we create a Sass function.

@function fade($c) {
  return linear-gradient($c, rgba($c, 0) 80%);
}

div {
  border: solid 0.125em;
  border-image: fade(#be4c39) 1;
  padding: 0.125em;
  background: fade(#e18728) content-box;
}

We can test it live in this Pen:

This approach of using the padding to create the space between the background and the border is not the best unless we only have a short text in the middle. If we have a lot more text... well, it looks crappy.

The problem is that the text starts right from the edge of the orange background and we cannot add a padding because we've already used it for the transparent gap. We could add an extra element... or we could use box-shadow!

box-shadow can take 2, 3, or 4 length values. The first one is the x offset (determining how much to move the shadow to the right), the second one is the y offset (how much to move the shadow down), the third is the blur radius (determining how blurry the edge of the shadow is) and the fourth is the spread radius (determining how much the shadow expands in all directions).

The following interactive demo allows playing with these values — click any of them and a popup with a slider shows up.

Note that, while the blur radius has to be always greater or equal to zero, the offsets and the spread radius can be negative. A negative offset simply moves the shadow in the negative direction of its axis (left or up). A negative spread radius means the shadow shrinks instead of expanding.

Another important thing to notice here — because it's convenient for our use case — is that that the box-shadow is never visible underneath the space occupied by the border-box, not even when that space is (semi)transparent.

If we keep the offsets and the blur radius to zero, but give the spread radius a positive value, then we get what looks like a second solid border of equal width in all directions, starting from the limit of the actual border and going outwards.


emulate border with box-shadow

Note that this kind of border doesn't necessarily need to have equal width in all directions. We can give it different border widths by tweaking the offset values and the spread radius, with the restriction that the sum of the widths of the horizontal borders equals the sum of the widths of the vertical borders. Using multiple shadows could help us get past this restriction if we don't need the border to be semitransparent, but this would be the kind of solution that complicates things instead of simplifying them.

Returning to our demo, we use the spread of the box-shadow to fake the border, use the actual border to create the transparent gap, set background-clip to padding-box, and let the padding do its job:

border: solid 1em transparent;
padding: 1em;
box-shadow: 0 0 0 1em #be4c39;
background: #e18728 padding-box;

highlighting the border, padding, and content area via dev tools

It can be seen in action in the following Pen:

We could also fake a border by emulating an inset shadow. In this case, it starts at the padding limit (the limit between the padding and the border areas) and goes inwards as much as the spread specifies:


emulate a second border with an inset box-shadow

Because we can have multiple box shadows, we can fake multiple borders this way. Let's take the case where we have two, one of them an inset one. If the actual border of the element is non-zero and transparent and the background-clip is set to padding-box, then we get a fake double border with a transparent area (the actual border area) between its inner and outer components. Note that this requires also increasing the padding to compensate for the space taken by the inset box-shadow.

border: solid 1em transparent;
padding: 2em; // increased from 1em to 2em to compensate for inner "border"
box-shadow: 
  0 0 0 0.25em #be4c39 /* outer "border" */,
  inset 0 0 0 1em #be4c39 /* inner "border" */;
background: #e18728 padding-box;

It can be tested live in this Pen, where we also have a drop-shadow() filter for a glow effect:

Single element (no pseudos) target with smooth edges

Let's say we want to get a target like the one below, with the restriction that we can only use one element and no pseudo-elements.


the target we want to CSS

The first idea would be to use a repeating-radial-gradient. Our target is structured something like this:


target structure illustration

So half the target would be 9 units, meaning the horizontal and vertical dimensions of the target are each 18 units. Our repeating radial gradient is black for the first unit, then transparent until the third unit, then black again and then transparent again... and this sounds like a repetition. Except we only have one unit from 0 to 1, the first time we have a black region, but then the second time we have a black region, it goes from 3 to 5 — that's two units! So... we shouldn't start from 0 there, but from -1 instead, right? Well, that should work, according to the spec.

$unit: 1em;

background: repeating-radial-gradient(
  #000 (-$unit), #000 $unit, 
  transparent $unit, transparent 3*$unit
);

This Pen illustrates the idea:

The first problem here is that IE has a different opinion on how this should work.


repeating-radial-gradient with negative first stop: expected result (left) vs. IE (right)

Luckily, this has been fixed in Edge, but if IE support is needed, it's still a problem. One that we can solve by using a plain radial gradient because we don't need that many circles anyway. It's more code, but it's not that bad...

background: radial-gradient(
  #000 $unit, transparent 0, 
  transparent 3*$unit, #000 0, 
  #000 5*$unit, transparent 0, 
  transparent 7*$unit, #000 0, 
  #000 9*$unit, transparent 0
);

We can see it in action in this Pen:

The circles are now distributed the same way in all browsers, but we still have another problem: the edges may not be as far from smooth as in the original image in IE/ Edge, but they look ugly in Firefox and Chrome!


original (top left) vs. IE/ Edge (top right) vs. Firefox (bottom left) vs. Chrome (bottom right)

We could use the non-sharp transition trick:

background: radial-gradient(
  #000 calc(#{$unit} - 1px), 
  transparent $unit, 
  transparent calc(#{3*$unit} - 1px), 
  #000 3*$unit, 
  #000 calc(#{5*$unit} - 1px), 
  transparent 5*$unit, 
  transparent calc(#{7*$unit} - 1px), 
  #000 7*$unit, 
  #000 calc(#{9*$unit} - 1px), 
  transparent 9*$unit
);

Live test:

Well, this improves things in IE (where the result already looked good) and Firefox, but the edges still look ugly in Chrome.


original (top left) vs. IE (top right) vs. Firefox (bottom left) vs. Chrome (bottom right)

Maybe radial gradients are not the best solution after all. What if we were to adapt the background-clip and box-shadow solution from the previous section to this problem? We can use an outer box-shadow for the outer circle and an inset one for the inner circle. The space between them gets taken by a transparent border. We also set background-clip to content-box and give the element enough padding so that we have a transparent area between the central disc and the inner circle.

border: solid 2*$unit transparent;
padding: 4*$unit;
width: 2*$unit; height: 2*$unit;
border-radius: 50%;
box-shadow: 
  0 0 0 2*$unit #000, 
  inset 0 0 0 2*$unit #000;
background: #000 content-box;

We can see it working in the following pen, no jagged edges and no trouble:

Real life-like looking controls

I first got this idea when trying to style a range input's thumb, track and, for non-WebKit browsers, progress (fill). Browsers provide pseudo-elements for these components.

For the track, we have -webkit-slider-runnable-track, -moz-range-track and -ms-track. For the thumb, -webkit-slider-thumb, -moz-range-thumb and -ms-thumb. And for the progress/ fill, we have -moz-range-progress, -ms-fill-lower (both to the left of the thumb) and -ms-fill-upper (to the right of the thumb). WebKit browsers don't provide a pseudo-element that would allow styling the part before the thumb different from the part after.

These look inconsistent and ugly, but what's even uglier is that we cannot list all the browser versions for the same component together to style them. Something like this won't work:

input[type='range']::-webkit-slider-thumb, 
input[type='range']::-moz-range-thumb, 
input[type='range']::-ms-thumb { /* styles here */ }

We have to always write them like this:

input[type='range']::-webkit-slider-thumb { /* styles here */ }
input[type='range']::-moz-range-thumb { /* styles here */ } 
input[type='range']::-ms-thumb { /* styles here */ }

Which looks like a very WET style of writing code and it actually is in a lot of cases—though given the many browser inconsistencies when it comes to sliders, it's also useful for leveling things across the field. My solution to this was to use a thumb() mixin and maybe give it arguments for handling inconsistencies. Something like this, for example:

@mixin thumb($flag: false) {
  /* styles */
	
  @if $flag { /* more styles */ }
}

input[type='range'] {
  &::-webkit-slider-thumb { @include thumb(true); }
  &::-moz-range-thumb { @include thumb(); } 
  &::-ms-thumb { @include thumb(); }
}

But let's go back to how we can style things. We can only add pseudo-elements to these components for Chrome/ Opera, which means that, in reproducing their look, we have to get as close as possible to it without relying on pseudo-elements. This leaves backgrounds, borders, shadows, filters on the thumb element itself.

Let's see a few examples!

Soft plastic control

For a visual example, think something like the thumb of the slider below:


slider with soft plastic thumb

First thought would be it's as simple as a gradient background and a gradient for border-image, then just drop a box-shadow there and that's it for such a control:

border: solid 0.375em;
border-image: linear-gradient(#fdfdfd, #c4c4c4) 1;
box-shadow: 0 0.375em 0.5em -0.125em #808080;
background: linear-gradient(#c5c5c5, #efefef);

And this does work (using a button element instead of the slider thumb to simplify things):

Except our control is round, not square. We'd just need to set border-radius: 50% on it then, right? Well... that doesn't work because we're using border-image, which makes border-radius be ignored on the element itself, though, funny enough, it still gets applied on the box-shadow, if there is one.

What should we do then? Well, use background-clip! We first give the element a non-zero padding, no border and make it round with border-radius: 50%. Then we layer two gradient backgrounds, the top one being restricted to the content-box (note the clipping is being applied as part of the background shorthand). Finally, we add two box shadows, the first one being a dark one that creates the shadow underneath the control and the second being an inset one, that should darken a bit the bottom and laterals of the control's outer part.

border: none; /* makes border-box ≡ padding-box */
padding: .375em;
border-radius: 50%;
box-shadow: 0 .375em .5em -.125em #808080, 
      inset 0 -.25em .5em -.125em #bbb;
background: 
  linear-gradient(#c5c5c5, #efefef) content-box, 
  linear-gradient(#fdfdfd, #c4c4c4);

The final result can be seen in this Pen:

Matte control

Something like the thumb of the slider in the following image:


slider with matte thumb

This looks pretty similar to the previous case, except now we have a lighter line at the top and an inset shadow for the middle part. If we use an outer box-shadow to create the lighter line, the whole thing wouldn't be round anymore unless we also decrease its height to compensate for the shadow. That would mean we need to do more computations to determine the position of the inner part. If we use an inset one instead, then we can't use that for the dark shadow of the inner part. However, we could emulate that with a radial-gradient that gets conveniently sized, positioned, and clipped to the content-box. This means the same strategy as in the previous case, except we have an extra radial-gradient layered on top of the other backgrounds. The actual background-size of the radial gradient is larger than the content-box so we can shift it down without bringing its top edge within the content limit.

border: none; /* makes border-box ≡ padding-box */
padding: .625em;
width: 1.75em; height: 1.75em;
border-radius: 50%;
box-shadow: 
  0 1px .125em #444 /* dark lower shadow */, 
  inset 0 1px .125em #fff /* light top hint */;
background: 
  /* inner shadow effect */
  radial-gradient(transparent 35%, #444) 
    50% calc(50% + .125em) content-box, 
  
  /* inner background */
  linear-gradient(#bbb, #bbb) content-box, 
  
  /* outer background */
  linear-gradient(#d0d3d5, #d2d5d7);
background-size: 
  175% 175% /* make radial-gradient bg larger */, 
  100% 100%, 100% 100%;

We can see it live in this Pen:

3D control

For example, the thumb of the following slider:


slider with 3D thumb

This one is a bit more complex and requires that the three boxes (the content-box, the padding-box, and the border-box) are all different, so that we can layer backgrounds and use background-clip to get the desired effect.

So for the main part of the slider, we have a gradient background clipped to the content-box layered on top of another clipped to the padding-box, both of them over a third linear-gradient clipped to border-box. We also make use of inset box-shadow to highlight the padding limit (between the padding and the border):

border: solid .25em transparent;
padding: .25em;
border-radius: 1.375em;
box-shadow: 
  inset 0 1px 1px rgba(#f7f7f7, .875) /* top */, 
  inset 0 -1px 1px rgba(#bbb, .75) /* bottom */;
background: 
  linear-gradient(#9ea1a6, #fdfdfe) content-box, 
  linear-gradient(#fff, #9c9fa4) padding-box, 
  linear-gradient(#eee, #a4a7ab) border-box;

The result for this part can be seen in this Pen:

Now what's left is the little round part. This is one case where I felt a pseudo-element was really needed. I did end up taking that route for Blink, but managed to get a decent-looking fallback for the rest of the browsers by layering two radial gradients on top of the linear ones:

We might be able to get even closer to what we want with better chosen shades, extra radial gradients, or even using background-blend-mode, but I don't have the artistic sense needed for something like that.

With a pseudo-element, it's a lot easier to get the desired result — we first need to position and size it properly, make it round with border-radius: 50%. Then we give it a padding, no border, and use two gradients for the background, the top one being a radial one clipped to the content-box:

padding: .125em;
background: 
  radial-gradient(circle at 50% 10%, 
      #f7f8fa, #9a9b9f) content-box, 
  linear-gradient(#ddd, #bbb);

The result for this can be seen in the following Pen:

For the actual slider thumb, I used the same thumb background with the radial gradients on top everywhere, and added the pseudo for Blink right on top of the radial gradients. This is because thumb slider styles in Safari get applied via ::-webkit-slider-thumb, but Safari doesn't support pseudo-elements on the thumb (or track). So if I were to remove the fallback from the styles applied to ::-webkit-slider-thumb, then Safari wouldn't display the round part at all.

Illusion of depth

The track of the following slider illustrates this idea:


slider with track having depth

Just like before, we do this by giving the element a non-zero transparent border, a padding, and layering backgrounds with different background-clip values (remember that those with content-box values need to be on top of those with padding-box values, which need to be on top of those with border-box values). In this case, we have a lighter linear-gradient to cover the border area, a darker one plus a couple of radial ones which we make smaller and non-repeating to darken the ends even further for the padding area, and a really dark one for the content area. Then we give this a border-radius equal to at least half the height of the content area plus twice the padding and twice the border. We also add an inset box-shadow to subtly highlight the padding limit.

border: solid .375em transparent;
padding: 1em 3em;
width: 15.75em; height: 1.75em;
border-radius: 2.25em;
background: 
  linear-gradient(#090909, #090909) content-box, 
  radial-gradient(at 5% 40%, #0b0b0b, transparent 70%) 
    no-repeat 0 35% padding-box /* left */, 
  radial-gradient(at 95% 40%, #111, transparent 70%) 
    no-repeat 100% 35% padding-box /* right */, 
  linear-gradient(90deg, #3a3a3a, #161616) padding-box,
  linear-gradient(90deg, #2b2d2c, #2a2c2b) border-box;
background-size: 100%, 9em 4.5em, 4.5em 4.5em, 100%, 100%;

But there's a problem with this, and it can be seen in the following Pen:

Due to the way border-radius works — the radius for the content area being the one we specify minus the border-width minus the padding, which ends up being negative — there is no rounding for the corners of the content area. Well, we can fix this! We can emulate the shape we want by using both linear and radial gradients for the content area.

The way we do this is by first making sure the width of our content area is a multiple of its height (in our case, 15.75em = 9*1.75em). We first layer a non-repeating linear-gradient positioned in the middle that covers the entire height of the content area vertically, but leaves a space of half the content area height at both the left and the right ends. On top of this we add a radial-gradient with a background-size equal to the content area height both horizontally and vertically.

Metallic controls

For example, something like the button illustrated below:


metallic control

This is a bit more complex, so let's break it down. First of all, we make the button circular by giving it equal width and height and setting border-radius: 50%. Then we make sure it has box-sizing: border-box, so that the border-width and the padding go inwards (get subtracted from the dimensions we have set). Now the next logical step is to give it a transparent border and a padding. So far, this is what we have:

$d-btn: 27em; /* control diameter */
$bw: 1.5em; /* border-width */

button {
  box-sizing: border-box;
  border: solid $bw transparent;
  padding: 1.5em;
  width: $d-btn; height: $d-btn;
  border-radius: 50%;
}

It doesn't look like anything yet and that's because the entire look is achieved with the help of just two properties we haven't added in yet: box-shadow and background.

Before doing anything else, let's deconstruct the control a bit:


deconstructing the metallic control

Starting from the outer part and going inwards, we have:

  • a thick outer ring with LEDs
  • a thin inner ring
  • a perforated area (that includes the cyan glow around the following inner part)
  • a big central part

The thick outer ring is the border area, the central part is the content area and everything in between (the thin inner ring and the perforated part) is the padding area. We create the thin inner ring with inset box shadows:

box-shadow: 
  /* discrete dark shadow to act as separator from outer ring */
  inset 0 0 1px #666, 

  /* darker top area */
  inset 0 1px .125em #8b8b8b, 
  inset 0 2px .25em #a4a2a3, 

  /* darker bottom area */
  inset 0 -1px .125em #8b8b8b, 
  inset 0 -2px .25em #a4a2a3, 

  /* the base circular strip for the inner ring */
  inset 0 0 0 .375em #cdcdcd;

We also add two more outer box shadows, one for the lighter top highlight of the outer ring and the second for the discrete dark shadow below the control, so we now have:

box-shadow: 
  0 -1px 1px #eee, 
  0 2px 2px #1d1d1d, 
  inset 0 0 1px #666, 
  inset 0 1px .125em #8b8b8b, 
  inset 0 2px .25em #a4a2a3, 
  inset 0 -1px .125em #8b8b8b, 
  inset 0 -2px .25em #a4a2a3, 
  inset 0 0 0 .375em #cdcdcd;

Still not much, but it's more than before:

Now we have to layer three types of backgrounds, from top to bottom: limited to the content-box (creating the central area), limited to the padding-box (creating the perforated area and cyan glow) and limited to the border-box (creating the thick outer ring and LEDs).

We start with the central area, where we have some discrete circular lines created with three repeating radial gradients stacked one on top of the other, with the values of the stops based on the cicada principle and conic reflections created with a conic gradient. Note that conic gradients are not yet supported in any browser so we need to use a polyfill at this point.

background:
  /* ======= content-box ======= */
  /* circular lines - 13, 19, 23 being prime numbers */
  repeating-radial-gradient(
      rgba(#e4e4e4, 0) 0, 
      rgba(#e4e4e4, 0) 23px, 
      rgba(#e4e4e4, .05) 25px, 
      rgba(#e4e4e4, 0) 27px) content-box, 
  repeating-radial-gradient(
      rgba(#a6a6a6, 0) 0, 
      rgba(#a6a6a6, 0) 13px, 
      rgba(#a6a6a6, .05) 15px, 
      rgba(#a6a6a6, 0) 17px) content-box, 
  repeating-radial-gradient(
      rgba(#8b8b8b, 0) 0, 
      rgba(#8b8b8b, 0) 19px, 
      rgba(#8b8b8b, .05) 21px, 
      rgba(#8b8b8b, 0) 23px) content-box, 
  /* conic reflections */
  conic-gradient(/* random variations of some shades of grey */
      #cdcdcd, #9d9d9d, #808080, 
      #bcbcbc, #c4c4c4, #e6e6e6, 
      #dddddd, #a1a1a1, #7f7f7f, 
      #8b8b8b, #bfbfbf, #e3e3e3, 
      #d2d2d2, #a6a6a6, #858585, 
      #8d8d8d, #c0c0c0, #e5e5e5, 
      #d6d6d6, #9e9e9e, #828282, 
      #8f8f8f, #bdbdbd, #e3e3e3, #cdcdcd) 
    content-box;

Now this is finally starting to look like something!

We move on to the perforated area. The cyan glow is just a radial gradient to transparency in the outer part, while the perforations are based on the Carbon fibre pattern from the gallery Lea Verou put together some five years ago - still damn useful for artistically challenged people such as myself.

$d-hole: 1.25em; /* perforation diameter*/
$r-hole: .5*$d-hole; /* perforation radius */

background: 
  /* ======= padding-box ======= */
  /* cyan glow */
  radial-gradient(
      #00d7ff 53%, transparent 65%) padding-box, 
  /* holes */
  radial-gradient(
      #272727 20%, transparent 25%) 
    0 0 / #{$d-hole} #{$d-hole} 
    padding-box,
  radial-gradient(
      #272727 20%, transparent 25%) 
    $r-hole $r-hole / #{$d-hole} #{$d-hole} 
    padding-box,
  radial-gradient(#444 20%, transparent 28%) 
    0 .125em / #{$d-hole} #{$d-hole} 
    padding-box,
  radial-gradient(#444 20%, #3d3d3d 28%) 
    #{$r-hole} #{$r-hole + .125em} / #{$d-hole} #{$d-hole} 
    padding-box

It looks like we're getting close to something decent looking:

The basic thick outer ring (without the LEDs) is created with a single conic gradient:

conic-gradient(
  #b5b5b5, #8d8d8d, #838383, 
  #ababab, #d7d7d7, #e3e3e3, 
  #aeaeae, #8f8f8f, #878787, 
  #acacac, #d7d7d7, #dddddd, 
  #b8b8b8, #8e8e8e, #848484, 
  #a6a6a6, #d8d8d8, #e3e3e3, 
  #8e8e8e, #868686, #a8a8a8, 
  #d5d5d5, #dedede, #b5b5b5) border-box;

We now have a metallic control!

It has no LEDs at this point, so let's fix that!

Every LED is made up of two non-repeating radial gradients stacked one on top of the other. The top one is the actual LED, while the bottom one, slightly offset vertically, creates the lighter highlight in the lower part of the LED. It's pretty much the same effect used for the holes in the perforated area. The bottom gradient is always the same, but the top one differs depending on whether the LEDs are on or off.

We take the LEDs to be on up to the $k-th one. So up to the point we use the cyan variation for the top gradient, while after that we use the grey one.

We have 24 LEDs that are positioned on a circle passing through the middle of its border area. So its radius is the radius of the control minus half the border width.

We generate all these gradients with Sass. We first create an empty list of gradients, then we loop and, for every iteration, we add two gradients to the list. Their positions are computed so they're on the previously mentioned circle. The first gradient depends on the loop index, while the second one is always the same (only at another position on the circle).

$d-btn: 27em;
$bw: 1.5em;
$r-pos: .5*($d-btn - $bw);
$n-leds: 24;
$ba-led: 360deg/$n-leds;
$d-led: 1em;
$r-led: .5*$d-led;
$k: 7;
$leds: ();

@for $i from 0 to $n-leds {
  $a: $i*$ba-led - 90deg;
  $x: .5*$d-btn + $r-pos*cos($a) - $r-led;
  $y: .5*$d-btn + $r-pos*sin($a) - $r-led;
  $leds: $leds, 
    if($i < $k, 
      (radial-gradient(circle, #01d6ff, 
          #178b98 .5*$r-led, 
          rgba(#01d6ff, .35) .7*$r-led, 
          rgba(#01d6ff, 0) 1.3*$r-led) no-repeat 
        #{$x - $r-led} #{$y - $r-led} / 
        #{2*$d-led} #{2*$d-led} border-box), 
      (radial-gradient(circle, #898989, 
          #4d4d4d .5*$r-led, #999 .65*$r-led, 
          rgba(#999, 0) .7*$r-led) no-repeat 
        $x $y / #{$d-led} #{$d-led} border-box)
    ), 
    radial-gradient(circle, 
        rgba(#e8e8e8, .5) .5*$r-led, 
        rgba(#e8e8e8, 0) .7*$r-led) no-repeat 
      $x ($y + .125em) / #{$d-led} #{$d-led} 
      border-box;
}

The final result can be seen in this Pen:

Shadows in a perpendicular plane

Consider the case of controls being in the vertical plane of the screen, for which we want to have a shadow in a horizontal plane below. Something like in the following image:


controls with shadow in a horizontal plane below them

What we want is to recreate this effect using just one element and no pseudo-elements.

Layering backgrounds with different background-clip and background-origin values does the trick in this case as well. We create the actual button with two backgrounds, the one on top clipped to the content-box and the one under it clipped to the padding-box and use a radial-gradient() background with background-clip and background-origin set to border-box to create the shadow.

The basic styling is pretty similar to that of the metallic control in the previous section:

$l: 6.25em;
$bw: .1*$l;

border: solid $bw transparent;
padding: 3px;
width: $l; height: $l;
border-radius: 1.75*$bw;

We give it a thickish transparent border all around, so that we have enough space to recreate that shadow in the bottom border area. We do this for all borders, not just for the bottom one because we want the same kind of symmetrical rounding for all the corners of the padding-box (if you need a refresher of how this works, check out Lea Verou's excellent border-radius talk).

The first background from the top is a conic-gradient() one to create the conic metal reflections. This one is clipped to the content-box. Right underneath it, we have a simple linear-gradient() clipped to the padding-box. We use three inset box shadows to make this second background less flat - add another shade all around with a zero blur, positive spread shadow, make it lighter at the top with a semitransparent white shadow and darker at the bottom with a semitransparent black shadow.

box-shadow: 
  inset 0 0 0 1px #eedc00, 
  inset 0  1px 2px rgba(#fff, .5), 
  inset 0 -1px 2px rgba(#000, .5);
background: 
  conic-gradient(
      #edc800, #e3b600, #f3cf00, #ffe800, 
      #ffe900, #ffeb00, #ffe000, #ebc500, 
      #e0b100, #f1cc00, #fcdc00, #ffe500, 
      #fad900, #eec200, #e7b900, #f7d300, 
      #ffe800, #ffe300, #f5d100, #e6b900, 
      #e3b600, #f4d000, #ffe400, #ebc600, 
      #e3b600, #f6d500, #ffe900, #ffe90a, 
      #edc800) content-box, 
  linear-gradient(#f6d600, #f6d600) padding-box

This gives us the metallic button (without the shadow yet):

For the shadow, layer a third background for which we set both background-clip and background-origin to border-box. This background is a non-repeating radial-gradient() whose position we attach to the bottom (and horizontally in the middle) and which we shrink vertically so that it fits into that bottom border area and even leaves a bit of space - so we take it for example to be something like .75 of the border-width.

radial-gradient(rgba(#787878, .9), rgba(#787878, 0) 70%) 
  50% bottom / 80% .75*$bw no-repeat border-box

And this is it! You can play with the buttons in the following Pen:


Background-clip certainly has its use cases! Particularly when layering multiple effects around the edges of elements.

in `_layouts/post.html`:

<h3>Leave a comment</h3>

<form id="comment">
  <label for="message">Message</label>
  <textarea id="message"></textarea>

  <label for="name">Name</label>
  <input type="text" id="name">

  <label for="email">Email</label>
  <input type="text" id="email">

  <input type="submit" value="Post Comment">
</form>

To send the data to Firebase when the form is submitted, override the default submit listener in `/js/blog.js`:

$("#comment").submit(function() {
  postRef.push().set({
    name: $("#name").val(),
    message: $("#message").val(),
    md5Email: md5($("#email").val()),
    postedAt: Firebase.ServerValue.TIMESTAMP
  });

  $("input[type=text], textarea").val("");
  return false;
});

postRef.push() creates an array in Firebase if it doesn't exist and returns a reference to the first item. set saves the data to Firebase.

We store an MD5 of the email address to protect the privacy of commenters since the data is public. Gravatar uses MD5s to display profile images.

Instead of new Date().getTime() for the timestamp, we use Firebase.ServerValue.TIMESTAMP. This is a timestamp from Firebase servers which avoids timezone issues and forged requests.

Displaying Comments

Add a container to hold comments the above the comment form in _layouts/post.html:

<hr>

<div class="comments"></div>

Firebase has a reference to listen for new comments. The child_added event triggers for existing and new comments. We use the same event to render all comments.

child_added returns a current snapshot of the data. We get the data from the snapshot, format it into HTML then prepend it to <div class="comments"></div>.

postRef.on("child_added", function(snapshot) {
  var newComment = snapshot.val();
  $(".comments").prepend('<div class="comment">' +
    '<h4>' + newComment.name + '</h4>' +
    '<div class="profile-image"><img src="http://www.gravatar.com/avatar/' + newComment.md5Email + '?s=100&d=retro"/></div> ' +
    '' + moment(newComment.postedAt).fromNow() + '<p>' + newComment.message  + '</p></div>');
});

The Complete File

Save the complete file to `/js/blog.js`. Change <YOUR-APP-ID> to ID you recorded earlier.

$(function() {
  var ref = new Firebase("https://<YOUR-APP-ID>.firebaseio.com/"),
    postRef = ref.child(slugify(window.location.pathname));

    postRef.on("child_added", function(snapshot) {
      var newPost = snapshot.val();
      $(".comments").prepend('<div class="comment">' +
        '<h4>' + newPost.name + '</h4>' +
        '<div class="profile-image"><img src="http://www.gravatar.com/avatar/' + newPost.md5Email + '?s=100&d=retro"/></div> ' +
        '<span class="date">' + moment(newPost.postedAt).fromNow() + '</span><p>' + newPost.message  + '</p></div>');
    });

    $("#comment").submit(function() {
      postRef.push().set({
        name: $("#name").val(),
        message: $("#message").val(),
        md5Email: md5($("#email").val()),
        postedAt: Firebase.ServerValue.TIMESTAMP
      });

      $("input[type=text], textarea").val("");
      return false;
    });
});

function slugify(text) {
  return text.toString().toLowerCase().trim()
    .replace(/&/g, '-and-')
    .replace(/[\s\W-]+/g, '-')
    .replace(/[^a-zA-Z0-9-_]+/g,'');
}

The completed commenting system looks like this:

Try out a working demo here. Open two windows and post a comment, you'll see it appear in both windows straight away.

Security

At the moment, anyone can edit or delete comments. For basic security we'll make a rule that visitors can only add comments. In Firebase, open up the Security and Rules tab:

The current rules allow global reads and writes. To prevent Firebase deleting or writing data if it already exists, change .write to:

".write": "!data.exists()"

A full set of authentication options is available to build something more complex.

The Finished Site

With a few libraries and 31 lines of JavaScript, we have a full featured backend for blog comments working on a static website.

That brings us to the end of this series. In three short tutorials, we've gone from a static site to an updatable, live Jekyll site with our own commenting system.

This is a three-part series:

Part 1: Converting a Static Website To Jekyll
Part 2: Adding a Jekyll CMS with CloudCannon
Part 3: (This post) Creating a Firebase-Backed Commenting System