Skip to content

Template customization

In this section, we are going to dive inside the tailwind.config.js file located at the root of the project. Everything happens there, so we need to give you all the info you'll need to be able to customize it for your needs.

Configuration imports

The tailwind configuration file starts with commonjs (using require instead of import) import statements:

javascript
const colors = require("tailwindcss/colors");
const plugin = require("tailwindcss/plugin");

We are doing 2 things:

  • We first load the colors object from tailwind to be able to access the native colors later in the configuration file
  • We then load the plugin object to be able to use tailwind plugins later in the file.

Dark mode & Content

We then take care of configuring the Dark mode as well as the files to parse, looking for tailwind class names to compile in the final bundle:

javascript
module.exports = {
  darkMode: "class",
  content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
  // ...rest of config
};

We are doing 2 things:

  • We are first telling tailwind to use the class implementation of the dark mode. This means that when the dark mode is toggled, a .dark class is added to the html DOM element, affecting all the application styles and colors. See Tailwind docs for more details about dark mode.
html
<!--Dark mode disabled-->
<html></html>

<!--Dark mode enabled-->
<html class="dark"></html>
  • We then tell tailwind to look for classes to compile inside all files residing inside the project folders, including files ending with different extensions. Learn more about content parsing by taking a look at the Tailwind docs.

Extending the base theme

The tailwind configuration file provides a very convenient way of overwriting the default theme and set your own variables. Everything related to theme customization goes inside the extend theme section of the configuration file:

javascript
module.exports = {
  // ...previous config elements
  theme: {
    extend: {
      // The theme configuration goes here.
    },
  },
  // ...rest of config
};

There are several things we can change in there. If you want more details about the tailwind configuration, you can always read the dedicated section of the Tailwind docs. Let's get back to Folio and deep dive in each subsection of the theme configuration.

Breakpoints

In Tailwind CSS, responsive breakpoints are known as screens. Being a mobile first framework, screens are set in an ascending manner, so you always design mobile first. Here is the default Tailwind configuration:

javascript
module.exports = {
  // ...previous config elements
  theme: {
    extend: {
      screens: {
        sm: "640px",
        md: "768px",
        lg: "1024px",
        xl: "1280px",
        xxl: "1536px",
      },
    },
  },
  // ...rest of config
};

While the defaults are pretty satisfying, we though that we could tweak them a little bit to get the most out of it, and to be able to face those very particular edge cases that come up from time to time. Here is Folio's screens configuration:

javascript
module.exports = {
  // ...previous config elements
  theme: {
    extend: {
      screens: {
        xs: { max: "639px" },
        sm: "640px",
        md: "768px",
        lg: "1025px",
        xl: "1280px",
        xxl: "1536px",
        ptablet: {
          raw: "(min-width: 768px) and (max-width: 1024px) and (orientation: portrait)",
        },
        ltablet: {
          raw: "(min-width: 768px) and (max-width: 1024px) and (orientation: landscape)",
        },
      },
    },
  },
  // ...rest of config
};

Let's go through the changes we've made. We've added an xs screen that, opposed to the others that follow an ascending pattern, is capped to 639px to face some very rare edge cases where you'd like to have something only applied on very small screens. We've also decided to push the lg breakpoint to 1025px instead of 1024px to exclude landscape tablets from desktop display, simply because there is not as much room on a landscape tablet as there is on a desktop screen, implying therefore layout adjustments between the two. And finally, we've added two raw screens called ptblet (stands for portrait tablet) and ltablet (stands for landscape tablet) to handle specific layouts rendered in both orientations at maximum design quality.

Colors

Tailwind CSS provides a very advanced and fully customizable color system. If you don't know anything about Tailwind colors, we recommend that you take a look at . the Tailwind docs to learn more about them. In Folio, we use the theme section of the configuration file to configure our own colors, that we will be using in the template. It also means, that you can do the same thing to change all the template colors to match your branding. Here is the color configuration of Folio:

javascript
module.exports = {
  // ...previous config elements
  theme: {
    extend: {
      colors: {
        slate: {
          1000: "#0a101f",
        },
        gray: {
          1000: "#080c14",
        },
        zinc: {
          1000: "#101012",
        },
        neutral: {
          1000: "#080808",
        },
        stone: {
          1000: "#0f0d0c",
        },
        muted: {
          ...colors.slate,
          1000: "#0a101f",
        },
        primary: colors.violet,
        info: colors.sky,
        success: colors.teal,
        warning: colors.amber,
        danger: colors.rose,
      },
    },
  },
  // ...rest of config
};

Tailwind CSS provides 22 pre-built colors, each one having 10 different shades, from the lightest one to the darkest one. Let's talk about some of our choices. First, even if the system is pretty complete, we felt it needed an additional really darker shade to enhance dark mode renders. Therefore we decided to add an additional shade to the gray based Tailwind colors.

WARNING

Consider the slate, gray, zinc, neutral and stone colors like base grey-ish backgrounds and tints for your design system and UI elements. Avoid at all costs mixing them as it will affect the consistency of your color system. Instead use one of those consistently throughout your design.

Following the gold rule in the above warning, we've decided to abstract the gray color we want to use for the template inside the muted variable.

javascript
  muted: colors.slate,

That simply is a shortcut to use our slate color through the template without taking the risk of mixing with another tint of gray. Therefore, instead of doing:

html
<span class="text-slate-600"></span>

We now simply write:

html
<span class="text-muted-600"></span>

Yeah, we pretty much know what you're going to say: "Hold on, this is pretty and nice, but how is it going to help me with my development? What benefit is this bringing?".

  • First, you don't take the risk of mixing inconsistent gray shades, having gray in some parts, slate in others, and maybe stone somewhere else. The main gray shade is abstracted in the muted color variable and prevents any potential errors, like when multiple people work on the same project.
  • Second, if you want to change the overall look of your app, you can do it anytime. You just need to switch the color linked to the muted variable, like this:
javascript
  // If you want a slate dominant in your layout
  muted: colors.slate,

  // If you want a gray dominant in your layout
  muted: colors.gray,

  // If you want a zinc dominant in your layout
  muted: colors.zinc,

  // If you want a neutral dominant in your layout
  muted: colors.neutral,

  // If you want a stone dominant in your layout
  muted: colors.stone,

we use the same mechanism to abstract other colors that you might need in a web project, like a primary color or other colors like success and danger that symbolize some application states. You can configure those colors as shortcuts as well:

javascript
  //The template primary color
  primary: colors.indigo,

  //The blue-ish info state
  info: colors.sky,

  //The green-ish success state
  success: colors.teal,

  //The orange-ish warning state
  warning: colors.amber,

  //The red-ish danger state
  danger: colors.rose,

Therefore, instead of using the indigo color, we abstract it to primary:

html
<!--You shouldn't do that-->
<span class="text-indigo-600"></span>

<!--This is the right way-->
<span class="text-primary-600"></span>

You should now have a good understanding of how colors work in Folio. Take a look at how we use them inside the template to get a more precise idea of the possibilities.

Fonts

Tailwind CSS provides a very simple way to abstract your fonts, like we did with colors. If you don't know anything about Tailwind font families, we recommend that you take a look at the Tailwind docs to learn more about how to configure and use them. In Folio, we use the theme section of the configuration file to configure our own fonts. It also means, that you can do the same thing to change all the template fonts to match your branding. Here is the color configuration of Folio:

javascript
module.exports = {
  // ...previous config elements
  theme: {
    extend: {
      fontFamily: {
        sans: ["Inter", "sans-serif"],
        heading: ["Inter", "sans-serif"],
      },
    },
  },
  // ...rest of config
};

We are doing 2 things:

  • We are first telling tailwind to overwrite its base sans font stack and to use Roboto flex instead. You then simply call this font in your templates by using the font-sans class expression.
  • We then add a new utility for headings that is using the Inter font. You then call this font in your templates by using the font-heading class expression.

You could also follow the same pattern if you want to customize the native font-serif and font-mono stacks that Tailwind provides by default. You could add something like this in the configuration above:

javascript
module.exports = {
  // ...previous config elements
  theme: {
    extend: {
      fontFamily: {
        serif: ["Some Font Name", "serif"],
        mono: ["Some Font Name", "monospace"],
      },
    },
  },
  // ...rest of config
};

plugins

Like we saw in the very first section of this page, we are first importing the tailwind plugin object to be able to use plugins later in our configuration file.

javascript
const plugin = require("tailwindcss/plugin");

We can then use this plugin definition to actually load our plugins into Tailwind CSS. Keep in mind that those plugins need to be installed via pnpm or it will result in an error when running the project.

javascript
module.exports = {
  // ...previous config elements
  theme: {
    extend: {
      plugins: [
        require("@tailwindcss/typography"),
        require("@tailwindcss/aspect-ratio"),
        require("@tailwindcss/line-clamp"),
      ],
    },
  },
  // ...rest of config
};

In the sample above, we are loading the typography plugin, which we saw earlier, as well as the line-clamp, aspect ratio and @vidstack plugins.

  • The typography plugin is in charge of styling user generated HTML or markdown content. You can read more about it in the official Tailwind documentaion.
  • The line-clamp plugin is in charge of truncating lines of text, whether on a single or multi-line basis. You can read more about it in the official Tailwind blog.
  • The aspect-ratio plugin is in charge of rendering correct aspect ratios like 1:1, 4:3 or 16:9. You can read more about it in the official Tailwind documentation.

Adding plugin utilities

we can also use the plugin object to add flat utilities directly in the configuration file:

javascript
module.exports = {
  // ...previous config elements
  theme: {
    extend: {
      plugins: [
        plugin(function ({ addUtilities }) {
          addUtilities({
            ".slimscroll::-webkit-scrollbar": {
              width: "6px",
            },
            ".slimscroll::-webkit-scrollbar-thumb": {
              borderRadius: ".75rem",
              background: "rgba(0, 0, 0, 0.1)",
            },
            ".slimscroll-opaque::-webkit-scrollbar-thumb": {
              background: "rgba(0, 0, 0, 0) !important",
            },
            ".mask": {
              "mask-size": "contain",
              "mask-repeat": "no-repeat",
              "mask-position": "center",
            },
            ".mask-blob": {
              "mask-image":
                "url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAwIDBDMjAgMCAwIDIwIDAgMTAwczIwIDEwMCAxMDAgMTAwIDEwMC0yMCAxMDAtMTAwUzE4MCAwIDEwMCAweiIvPjwvc3ZnPg==')",
            },
          });
        }),
      ],
    },
  },
  // ...rest of config
};

The classes added above will are available everywhere in the project and are also compatible with the Tailwind responsive-variants that rely on the breakpoints you've defined earlier.

Expose Tailwind colors as CSS variables

we can also use the plugin object to expose Tailwind colors as native CSS variables from the configuration file:

javascript
module.exports = {
  // ...previous config elements
  theme: {
    extend: {
      plugins: [
        function ({ addBase, theme }) {
          function extractColorVars(colorObj, colorGroup = "") {
            return Object.keys(colorObj).reduce((vars, colorKey) => {
              const value = colorObj[colorKey];

              const newVars =
                typeof value === "string"
                  ? { [`--color${colorGroup}-${colorKey}`]: value }
                  : extractColorVars(value, `-${colorKey}`);

              return { ...vars, ...newVars };
            }, {});
          }

          addBase({
            ":root": extractColorVars(theme("colors")),
          });
        },
      ],
    },
  },
  // ...rest of config
};

Tailwind colors are now available anywhere. For example, to get the bg-indigo-500 tailwind color, you simply need to reference a CSS variable like this: var(--color-indigo-500). That's it.

Complete configuration

That's it! Now that we've been through the entire Tailwind configuration file, you should have a solid understanding of what's happening in there, how to make changes to adapt it to your context and to get the most out of it. Here is the complete configuration that we've been through:

javascript
/** @type {import('tailwindcss').Config} */
const colors = require('tailwindcss/colors')
const plugin = require('tailwindcss/plugin')

module.exports = {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  darkMode: 'class',
  theme: {
    extend: {
      screens: {
        xs: { max: '639px' },
        sm: '640px',
        md: '768px',
        lg: '1025px',
        xl: '1280px',
        xxl: '1536px',
        ptablet: {
          raw: '(min-width: 768px) and (max-width: 1024px) and (orientation: portrait)',
        },
        ltablet: {
          raw: '(min-width: 768px) and (max-width: 1024px) and (orientation: landscape)',
        },
      },
      colors: {
        slate: {
          1000: '#0a101f',
        },
        gray: {
          1000: '#080c14',
        },
        zinc: {
          1000: '#101012',
        },
        neutral: {
          1000: '#080808',
        },
        stone: {
          1000: '#0f0d0c',
        },
        muted: {
          ...colors.slate,
          1000: '#0a101f',
        },
        primary: colors.violet,
        info: colors.sky,
        success: colors.teal,
        warning: colors.amber,
        danger: colors.rose,
      },
      fontFamily: {
        sans: ['Roboto Flex', 'sans-serif'],
        heading: ['Inter', 'sans-serif'],
      },
    },
  },
  plugins: [
    require('@tailwindcss/typography'),
    require('@tailwindcss/aspect-ratio'),
    plugin(function ({ addUtilities }) {
      addUtilities({
        '.slimscroll::-webkit-scrollbar': {
          width: '6px',
        },
        '.slimscroll::-webkit-scrollbar-thumb': {
          borderRadius: '.75rem',
          background: 'rgba(0, 0, 0, 0.1)',
        },
        '.slimscroll-opaque::-webkit-scrollbar-thumb': {
          background: 'rgba(0, 0, 0, 0) !important',
        },
        '.mask': {
          'mask-size': 'contain',
          'mask-repeat': 'no-repeat',
          'mask-position': 'center',
        },
        '.mask-blob': {
          'mask-image':
            "url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAwIDBDMjAgMCAwIDIwIDAgMTAwczIwIDEwMCAxMDAgMTAwIDEwMC0yMCAxMDAtMTAwUzE4MCAwIDEwMCAweiIvPjwvc3ZnPg==')",
        },
      })
    }),
    function ({ addBase, theme }) {
      function extractColorVars(colorObj, colorGroup = '') {
        return Object.keys(colorObj).reduce((vars, colorKey) => {
          const value = colorObj[colorKey]

          const newVars =
            typeof value === 'string'
              ? { [`--color${colorGroup}-${colorKey}`]: value }
              : extractColorVars(value, `-${colorKey}`)

          return { ...vars, ...newVars }
        }, {})
      }

      addBase({
        ':root': extractColorVars(theme('colors')),
      })
    },
  ],
}

All Rights Reserved