Building a Jekyll Site – Part 3 of 3: Creating a Firebase-Backed Commenting System | CSS-Tricks
Save article ToRead Archive Delete · Log in 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 Before anything else, let's see what
In the following image, we have an element's box model. If the If the If both the By default, backgrounds cover the entire 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 In this demo, we can see that the gradient background covers the entire When setting the We can make the background cover just the In the default case of If we set And finally, if we have These three situations are illustrated by the following live demo: We also have another property called Let's consider that we have an element like before, with a hashed The following demo illustrates what happens for each of the three possible values for The In the All right! Now let's see how we can use this to our advantage! Some may remember we can get semitransparent borders with Clipping the background to We can test it live with this Pen: We can make things more interesting by adding a Prefix reminder: I've seen a lot of resources adding We can also get a cool looking effect if we use a gradient for both the 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 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 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. 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 It can be seen in action in the following Pen: We could also fake a border by emulating an 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 It can be tested live in this Pen, where we also have a 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 first idea would be to use a So half the target would be This Pen illustrates the idea: The first problem here is that IE has a different opinion on how this should work. 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... 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! We could use the non-sharp transition trick: Live test: Well, this improves things in IE (where the result already looked good) and Firefox, but the edges still look ugly in Chrome. Maybe radial gradients are not the best solution after all. What if we were to adapt the We can see it working in the following pen, no jagged edges and no trouble: 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 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: We have to always write them like this: 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 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! For a visual example, think something like the thumb of the slider below: First thought would be it's as simple as a gradient And this does work (using a Except our control is round, not square. We'd just need to set What should we do then? Well, use The final result can be seen in this Pen: Something like the thumb of the slider in the following image: 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 We can see it live in this Pen: For example, the thumb of the following slider: This one is a bit more complex and requires that the three boxes (the So for the main part of the slider, we have a gradient 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 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 The result for this can be seen in the following Pen: For the actual slider thumb, I used the same thumb The track of the following slider illustrates this idea: Just like before, we do this by giving the element a non-zero transparent But there's a problem with this, and it can be seen in the following Pen: Due to the way The way we do this is by first making sure the For example, something like the button illustrated below: This is a bit more complex, so let's break it down. First of all, we make the button circular by giving it equal 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: Before doing anything else, let's deconstruct the control a bit: Starting from the outer part and going inwards, we have: 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: 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: 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 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. 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. 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: 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 We have 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). The final result can be seen in this Pen: 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: What we want is to recreate this effect using just one element and no pseudo-elements. Layering backgrounds with different The basic styling is pretty similar to that of the metallic control in the previous section: 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 The first This gives us the metallic button (without the shadow yet): For the shadow, layer a third background for which we set both 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`: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.background-clip
is and what it does.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.border-width
is 0
, the border-box
is the same size as the padding-box
, and the border limit coincides with the padding limit.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.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
.background-size: 50% 50%
and a hashed border
(using border-image
) so we can still see through the hashes what's underneath the border: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
.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.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.background-clip: border-box
, the clipping region is the border-box
, so we have the background underneath the border as well.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: content-box
, the clipping region is the content-box
, so the background is only displayed within the content-box
limit.background-origin
that specifies which of the three boxes the background-position
(and background-size
, if expressed in %
) is relative to.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%;
background-origin
— border-box
, padding-box
, and content-box
: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
.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
).Transparent gap between border and background
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;
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.drop-shadow()
filter that gives the whole thing a yellowish glow:-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.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;
}
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).box-shadow
is never visible underneath the space occupied by the border-box
, not even when that space is (semi)transparent.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;
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: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;
drop-shadow()
filter for a glow effect:Single element (no pseudos) target with smooth edges
repeating-radial-gradient
. Our target is structured something like this: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
);
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
);
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
);
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;
Real life-like looking controls
-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.input[type='range']::-webkit-slider-thumb,
input[type='range']::-moz-range-thumb,
input[type='range']::-ms-thumb { /* styles here */ }
input[type='range']::-webkit-slider-thumb { /* styles here */ }
input[type='range']::-moz-range-thumb { /* styles here */ }
input[type='range']::-ms-thumb { /* styles here */ }
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(); }
}
Soft plastic control
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);
button
element instead of the slider thumb to simplify things):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.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);
Matte control
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%;
3D control
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. 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;
background-blend-mode
, but I don't have the artistic sense needed for something like that.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);
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
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%;
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.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
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%;
}
box-shadow
and background
.
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;
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;
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).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;
$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
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;
$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.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.$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;
}
Shadows in a perpendicular plane
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.$l: 6.25em;
$bw: .1*$l;
border: solid $bw transparent;
padding: 3px;
width: $l; height: $l;
border-radius: 1.75*$bw;
padding-box
(if you need a refresher of how this works, check out Lea Verou's excellent border-radius
talk).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
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
<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