Customize Since v1.1.0+

Halfmoon is built entirely using CSS variables (also known as CSS custom properties). There are close to 1,500 CSS variables, which means that almost everything can be customized by overriding a property, making it very easy to theme Halfmoon to fit your brand.

Customization demo #

Before getting into the nitty gritty details of how and why, let's look at a demo. The demo below contains a form inside a card. Click the button on the top right to change the theme. The code below the demo is for the page after the customization.

This demo does not work on Internet Explorer, because the browser does not support CSS variables. Please open this page using another browser to view the demo.

The above example is loaded in an iFrame, so a few things may look out of place depending on your browser settings and screen size. Open it in a new tab
HTML (for the card and its contents)
<div class="card w-400 mw-full m-0 position-relative"> <!-- w-400 = width: 40rem (400px), mw-full = max-width: 100%, m-0 = margin: 0, position-relative = position: relative -->
  <!-- Dark mode toggle (position absolute inside the card) -->
  <div class="position-absolute top-0 right-0 z-10 p-10"> <!-- position-absolute = position: absolute, top-0 = top: 0, right-0 = right: 0, z-10 = z-index: 10, p-10 = padding: 1rem (10px) -->
    <button class="btn btn-square" type="button" onclick="halfmoon.toggleDarkMode()">
      <i class="fa fa-moon-o" aria-hidden="true"></i>
      <span class="sr-only">Toggle dark mode</span> <!-- sr-only = only for screen readers -->
    </button>
  </div>
  <!-- Card title -->
  <h2 class="card-title">Please fill up this form</h2>
  <!-- Form (inside the card) -->
  <form action="..." method="...">
    <div class="form-group">
      <label for="full-name" class="required">Name</label>
      <input type="text" id="full-name" class="form-control" placeholder="Your full name" required="required">
    </div>
    <div class="form-group">
      <label for="profession" class="required">Profession</label>
      <input type="text" id="profession" class="form-control" placeholder="Your profession" required="required">
    </div>
    <input class="btn btn-primary btn-block" type="submit" value="Submit">
    <div class="text-right mt-10"> <!-- text-right = text-align: right, mt-10 = margin-top: 1rem (10px) -->
      <a href="#">Need help?</a>
    </div>
  </form>
</div>
CSS (for customization)
/* 
  Global scope 
*/
:root {
  /* Change primary color from blue to indigo */

  --primary-color: var(--indigo-color);                                     /* Before: var(--blue-color) */
  --primary-color-light: var(--indigo-color-light);                         /* Before: var(--blue-color-light) */
  --primary-color-very-light: var(--indigo-color-very-light);               /* Before: var(--blue-color-very-light) */
  --primary-color-dark: var(--indigo-color-dark);                           /* Before: var(--blue-color-dark) */
  --primary-color-very-dark: var(--indigo-color-very-dark);                 /* Before: var(--blue-color-very-dark) */
  --primary-box-shadow-color: var(--indigo-box-shadow-color);               /* Before: var(--blue-box-shadow-color) */
  --primary-box-shadow-color-darker: var(--indigo-box-shadow-color-darker); /* Before: var(--blue-box-shadow-color-darker) */
  --text-color-on-primary-color-bg: var(--text-color-on-indigo-color-bg);   /* Before: var(--text-color-on-blue-color-bg) */

  /* Card */

  --card-border-radius: 1.2rem;                           /* Before: var(--base-border-radius) */

  /* Card (light mode only) */

  --lm-card-bg-image: linear-gradient(#f7f7f7, #ced6e0);  /* Before: none */
  --lm-card-border-color: #ffffff;                        /* Before: rgba(0, 0, 0, 0.2) */
  --lm-card-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);    /* Before: none */

  /* Card (dark mode only) */

  --dm-card-bg-image: linear-gradient(#191c20, #111417);  /* Before: none */
  --dm-card-border-color: rgba(255, 255, 255, 0.1);       /* Before: rgba(0, 0, 0, 0.2) */
  --dm-card-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);     /* Before: none */

  /* Input (light mode only) */

  --lm-input-box-shadow: 0 0.2rem 0 rgba(0, 0, 0, 0.05);  /* Before: none */

  /* Input (dark mode only) */

  --dm-input-box-shadow: var(--lm-input-box-shadow);      /* Before: none */

  /* Primary button (light mode only) */

  --lm-button-primary-bg-image: linear-gradient(var(--primary-color), var(--primary-color-dark));             /* Before: none */
  --lm-button-primary-bg-image-hover: linear-gradient(var(--primary-color-light), var(--primary-color-dark)); /* Before: none */

  /* Primary button (dark mode only) */

  --dm-button-primary-bg-image: linear-gradient(var(--primary-color), var(--primary-color-dark));             /* Before: none */
  --dm-button-primary-bg-image-hover: linear-gradient(var(--primary-color-light), var(--primary-color-dark)); /* Before: none */
}

/* 
  Local scope 
  ~ This way, only the primary button's height and border width is affected.
  ~ Default button remains the same.
*/
.btn-primary {
  --button-height: 3.6rem;  /* Before: 3.2rem (32px) */
  --button-border-width: 0; /* Before: var(--base-border-width) */
}

Pretty cool, right? Changing only a handful of variables can drastically change the look and feel of your site. Don't worry if the code above does not make sense to you (yet). It's easier than it looks. Read through the next section which explains Halfmoon variables in details.

On a side note, the icon being used is part of the Font Awesome 4.7.0 iconset.

Understanding CSS variables in Halfmoon #

The next few sections will explain how the variables work in Halfmoon.

Before we begin

Before going through the next sections, you should ideally have a basic understanding of CSS variables. You can read this: Using CSS custom properties (variables) . It is a fairly simple concept, and should be easy to understand.

Naming and states #

In Halfmoon, variable names are long, descriptive, and self-explanatory. The only thing to understand about them are the states. A variable with a given state only applies to the element when the element is in that state.

Basic states

  • --lm-* variables apply only in light mode.
  • --dm-* variables apply only in dark mode.
  • Variables without these states apply for both modes.

Other common states

  • *-hover (:hover)
  • *-focus (:focus)
  • *-active (.active)
  • *-active-hover (:hover and .active)

While this may seem complicated at first, the easiest way to understand variable names is by simply looking at some examples, such as the ones you can find in the table below.

Example variable Description
--button-padding Sets the padding of buttons (in both light and dark mode)
--lm-button-primary-bg-color Sets the background-color of primary buttons (in light mode)
--dm-card-border-color Sets the border-color of cards (in dark mode)
--lm-pagination-item-text-color-hover Sets the color of pagination items when they are hovered over (in light mode)
--dm-sidebar-link-border-color-active-hover Sets the border-color of sidebar links when they are are hovered over, and have the class .active (in dark mode)
--small-button-height Sets the height of small buttons (.btn-sm)
--lm-input-box-shadow-focus Sets the box-shadow of inputs when they are focused (in light mode)
--dm-open-collapse-header-bg-color Sets the background-color of collapse headers when the collapse panels are open (in dark mode)

Hopefully it is obvious from the examples that simply reading the name of a variable is enough to understand exactly what it does. It is highly recommended that you go through the halfmoon-variables.css file to see all the available properties. You can download it from the download page (opens in new tab).

Scopes #

All the variables in Halfmoon are set in the global scope, ie, :root. This makes customization easy because there is no need to use selectors when creating themes; simply override the variables in the global scope and you are good to go.

Moreover, local scoping is also possible (as shown in the demo above), where you can override variables inside a particular selector to get a different style of component. Another example is shown below.

This demo does not work on Internet Explorer, because the browser does not support CSS variables. Please open this page using another browser to view the demo.

The above example is loaded in an iFrame, so a few things may look out of place depending on your browser settings and screen size. Open it in a new tab
HTML (for the progress bars)
<!-- Default progress bar -->
<div class="progress">
  <div class="progress-bar" role="progressbar" style="width: 60%" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<!-- Success progress bar -->
<div class="progress">
  <div class="progress-bar bg-success" role="progressbar" style="width: 60%" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<!-- Battery -->
<div class="progress battery">
  <div class="progress-bar bg-success" role="progressbar" style="width: 60%" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"></div>
</div>
CSS (for customization)
/* 
  Global scope 
*/
:root {
  --progress-height: 0.5rem; /* Before: 0.8rem (8px) */
}

/* 
  Battery (Progress bar with variables overridden in local scope) 
*/
.battery {
  --progress-height: 6rem; /* Before: 0.5rem (5px) */
  --progress-border-radius: 0.2rem; /* Before: 3.2rem (32px) */
}

In the example above, the height of all the progress bars is decresed from 0.8rem (8px) to 0.5rem (5px) by overriding the --progress-bar-height variable in the global scope (:root).

A new component is also created (.battery) which is designed to show the available battery percentage of a system. This is done by overriding the progress bar variables in local scope, ie, .battery {...}. This is a great example to understand how new components and styles can be created by simply overriding the existing variables in a local scope. With so many variables available, there is usually no need to write traditional CSS.

Given below is another example showing how local scoping can be used to create a contextual card.

This demo does not work on Internet Explorer, because the browser does not support CSS variables. Please open this page using another browser to view the demo.

The above example is loaded in an iFrame, so a few things may look out of place depending on your browser settings and screen size. Open it in a new tab
HTML (for the cards)
<!-- Default card -->
<div class="card w-400 mw-full"> <!-- w-400 = width: 40rem (400px), mw-full = max-width: 100% -->
  <h2 class="card-title m-0">Default card</h2> <!-- m-0 = margin: 0 -->
  <p class="mb-0"> <!-- mb-0 = margin-bottom: 0 -->
    The weather forecast didn't say that, but the steel plate in his hip did. He had learned over the years to trust his hip over the weatherman.
  </p>
</div>

<!-- Primary card -->
<div class="card card-primary w-400 mw-full"> <!-- w-400 = width: 40rem (400px), mw-full = max-width: 100% -->
  <h2 class="card-title m-0 text-smoothing-auto">Primary card</h2> <!-- m-0 = margin: 0, text-smoothing-auto = no anti-aliasing -->
  <p class="mb-0 text-smoothing-auto"> <!-- mb-0 = margin-bottom: 0, text-smoothing-auto = no anti-aliasing -->
    The weather forecast didn't say that, but the steel plate in his hip did. He had learned over the years to trust his hip over the weatherman.
  </p>
</div>
CSS (for customization)
/* 
  Primary card (Card with variables overridden in local scope)
*/
.card-primary {
  /* Card (light mode only) */

  /* --lm-card-text-color unchanged */
  --lm-card-bg-color: var(--primary-color-very-light); /* Before: var(--white-bg-color) */
  --lm-card-border-color: var(--primary-color-light);  /* Before: rgba(0, 0, 0, 0.2) */

  /* Card (dark mode only) */

  --dm-card-text-color: var(--primary-color);         /* Before: var(--dm-base-text-color) */
  --dm-card-bg-color: var(--primary-color-very-dark); /* Before: var(--dark-color) */
  --dm-card-border-color: var(--primary-color-dark);  /* Before: rgba(0, 0, 0, 0.2) */
}

Please note that the .text-smoothing-auto class removes anti-aliasing from the text. You can learn more about anti-aliasing in the text utilities section (opens in new tab).

Colors #

Halfmoon comes with a bunch of colors. Each color has five different shades (including the base). These variables come in the following formats: --{name}-color, --{name}-color-light, --{name}-color-very-light, --{name}-color-dark, and --{name}-color-very-dark. The {name} is obviously the name of the color, for example, blue or red.

Each color also has two more variables for box shadows (using RGBA with varying opacity). These are mostly used for focus effects on elements. They come in the following format: --{name}-box-shadow-color and --{name}-box-shadow-color-darker. Moreover, each color also has a variable for a color that should be set as the color of text placed on the base shade of that color (--text-color-on-{name}-color-bg).

Colors in Halfmoon are divided into two categories:

System colors

Eight different system colors are available: blue, indigo, teal, green, yellow, orange, red and pink. Each color has eight different variables, for the shades, box shadows, and text color on base (as discussed above).

However, please note that system colors are not used in classes and components. They are instead extended by context colors which are then used in the components and utilities.

Context colors

Four different context colors are available: primary, success, secondary, and danger. Once again, each color has eight different variables, for the shades, box shadows, and text color on base (as discussed above). These colors are used in components and utilities. By default, each context color is set to a system color. The default values are as follows:

  • primary context color set to blue system color.
  • success context color set to green system color.
  • secondary context color set to yellow system color.
  • danger context color set to red system color.

You can override a context color by setting all of its variables to the corresponding variables of a system color (as shown in the very first example on this page). Another example is shown in the code below.

CSS (for overriding color)
/* 
  Global scope 
*/
:root {
  /* Change secondary color from yellow to pink */

  --secondary-color: var(--pink-color);                                     /* Before: var(--yellow-color) */
  --secondary-color-light: var(--pink-color-light);                         /* Before: var(--yellow-color-light) */
  --secondary-color-very-light: var(--pink-color-very-light);               /* Before: var(--yellow-color-very-light) */
  --secondary-color-dark: var(--pink-color-dark);                           /* Before: var(--yellow-color-dark) */
  --secondary-color-very-dark: var(--pink-color-very-dark);                 /* Before: var(--yellow-color-very-dark) */
  --secondary-box-shadow-color: var(--pink-box-shadow-color);               /* Before: var(--yellow-box-shadow-color) */
  --secondary-box-shadow-color-darker: var(--pink-box-shadow-color-darker); /* Before: var(--yellow-box-shadow-color-darker) */
  --text-color-on-secondary-color-bg: var(--text-color-on-pink-color-bg);   /* Before: var(--text-color-on-yellow-color-bg) */
}

Variable inheritance #

Variable inheritance is when the value of a CSS variable is a reference to another one. So if the value of the reference variable is changed, all the variables extending it will also update automatically without needing to be manually set.

This is a very simple, but powerful concept that makes building custom themes a breeze, and Halfmoon has a lot of smart use of variable inheritance. For instance, the example below shows a card with a button in it. It has been customized using the same variables shown in the very first example on this page. Click on the button to open a modal.

This demo does not work on Internet Explorer, because the browser does not support CSS variables. Please open this page using another browser to view the demo.

The above example is loaded in an iFrame, so a few things may look out of place depending on your browser settings and screen size. Open it in a new tab
HTML (for the card and modal)
<!-- Modal -->
<div class="modal" id="..." tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      ...
    </div>
  </div>
</div>

<!-- Card -->
<div class="card">
  ...
</div>
CSS (for customization)
/* 
  Global scope 
*/
:root {
  /* Change primary color from blue to indigo */

  --primary-color: var(--indigo-color);                                     /* Before: var(--blue-color) */
  --primary-color-light: var(--indigo-color-light);                         /* Before: var(--blue-color-light) */
  --primary-color-very-light: var(--indigo-color-very-light);               /* Before: var(--blue-color-very-light) */
  --primary-color-dark: var(--indigo-color-dark);                           /* Before: var(--blue-color-dark) */
  --primary-color-very-dark: var(--indigo-color-very-dark);                 /* Before: var(--blue-color-very-dark) */
  --primary-box-shadow-color: var(--indigo-box-shadow-color);               /* Before: var(--blue-box-shadow-color) */
  --primary-box-shadow-color-darker: var(--indigo-box-shadow-color-darker); /* Before: var(--blue-box-shadow-color-darker) */
  --text-color-on-primary-color-bg: var(--text-color-on-indigo-color-bg);   /* Before: var(--text-color-on-blue-color-bg) */

  /* Card */

  --card-border-radius: 1.2rem;                           /* Before: var(--base-border-radius) */

  /* Card (light mode only) */

  --lm-card-bg-image: linear-gradient(#f7f7f7, #ced6e0);  /* Before: none */
  --lm-card-border-color: #ffffff;                        /* Before: rgba(0, 0, 0, 0.2) */
  --lm-card-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);    /* Before: none */

  /* Card (dark mode only) */

  --dm-card-bg-image: linear-gradient(#191c20, #111417);  /* Before: none */
  --dm-card-border-color: rgba(255, 255, 255, 0.1);       /* Before: rgba(0, 0, 0, 0.2) */
  --dm-card-box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);     /* Before: none */

  /* Primary button (light mode only) */

  --lm-button-primary-bg-image: linear-gradient(var(--primary-color), var(--primary-color-dark));             /* Before: none */
  --lm-button-primary-bg-image-hover: linear-gradient(var(--primary-color-light), var(--primary-color-dark)); /* Before: none */

  /* Primary button (dark mode only) */

  --dm-button-primary-bg-image: linear-gradient(var(--primary-color), var(--primary-color-dark));             /* Before: none */
  --dm-button-primary-bg-image-hover: linear-gradient(var(--primary-color-light), var(--primary-color-dark)); /* Before: none */
}

/* 
  Local scope 
  ~ This way, only the primary button's height and border width is affected.
  ~ Default button remains the same.
*/
.btn-primary {
  --button-height: 3.6rem;  /* Before: 3.2rem (32px) */
  --button-border-width: 0; /* Before: var(--base-border-width) */
}

As can be seen above, even though modal variables have not been overridden, the modal has been automatically customized to look like the card, because the modal variables inherit (or extend) the card variables. This is amazingly powerful because you can override only a small set of variables, but the result is a consistent theme, thanks to variable inheritance.

Advantages over preprocessors #

Variables have been a staple of most CSS preprocessors, such as SASS, SCSS, or LESS. Moreover, other frameworks already provide preprocessor variables for customization (at least to some extent). However, native CSS variables (or CSS custom properties) have some advantages over these preprocessors. The next sections will break down these advantages.

Native browser support #

Preprocessor variables have to be compiled before the browser can understand them, so there is an extra compilation step where the preprocessor turns them into native CSS. CSS variables, on the other hand, work directly on browsers that support them, so there is no extra compilation step using JavaScript. You write your CSS and the browser immediately understands it.

Read and set with JavaScript #

This point ties into the previous one, but it is important enough to be stated separately. Because CSS variables are natively understood by the browser, they can be read and set (or changed) using JavaScript in runtime. This is shown in the code below.

Read and set CSS variables with JavaScript
// Get the <html> tag (for reading and setting variables in global scope)
var myElement = document.documentElement;

// Read CSS variable
getComputedStyle(myElement).getPropertyValue("--variable-name");

// Set CSS variable
myElement.style.setProperty("--variable-name", "value");

Imagine building a dashboard where the user is able to set their preferred colors, sidebar width, content padding, font size, mode (light or dark), and everything else in between. The settings are saved client side, and when the user loads your site, JavaScript is used to set the corresponding CSS variables according to the user's preferences.

This is the next level of user experience, because it goes beyond just creating a theme. It gives the users the ability to choose exactly how they prefer their design settings. This is only possible with a framework built entirely using CSS variables, and with Halfmoon, you can do this by writing a few lines of JavaScript.

Better use of CDNs #

In Halfmoon, a custom theme is a CSS file with overridden variables (as shown above). In most cases, this is going to be tiny in size, especially compared to the actual library. You include this file in your template, and pull in halfmoon-variables.css using a CDN.

That way, you are getting the speed and caching of a CDN for pulling in the Halfmoon library (which is a big file), and your custom theme can be served from your own server (or even within a <style> tag). This is only possible because there is no build/compile process, and would definitely not be possible with preprocessors.

Other benefits #

There are also other benefits that CSS variables have over preprocessors:

  • Preprocessor variables do not cascade.
  • Preprocessor variables cannot be redefined within media queries and understand on which selector to apply on.
  • CSS variables are future proof. They will continue to be supported and improved upon, because they are part of the CSS language specification.

If you want to learn more about these benefits, please refer to this article .

Notes on browser compatibility #

As of August of 2020, CSS variables have a global usage total of 95% according to Can I use . This means that they can be used with relative confidence because it is supported by most of the browsers in use today. However, it is still not 100% and that should be kept in mind.

Internet Explorer

CSS variables are not supported by Internet Explorer 11, and while there are quite a few hacks out there that try to fix this (such as polyfills), the most reliable way to make sure that your site works on IE11 is to simply avoid using CSS variables all together.

This means that if you want to customize Halfmoon to fit your brand (and still make it work on IE11), you have to do this — override the variables in halfmoon-variables.css to create your theme, and use this useful tool to flatten the variables.

You can learn more about Internet Explorer compatibility in the support docs section (opens in new tab).