My PostCSS Setup for Next.js Projects

Introduction

I've been using PostCSS with my Next.js projects for quite some time now. I previously relied on Less and Sass, but with the continuous advancement of CSS and the introduction of features like custom properties, calc(), Flexbox, and CSS Grid, I decided to shift my focus away from preprocessors.

The first time I heard about PostCSS was via Autoprefixer, but there's much more to PostCSS than Autoprefixer.

Once I understood how PostCSS could streamline my workflow, I was hooked.

In this brief guide, I'll share my go-to PostCSS setup that has proven to be invaluable for numerous Next.js projects, if you're coming from Sass or Less and are on the fence then you should find this useful.

PostCSS config

I always use a customized postcss config, I have my own preferences that are transferable to each project. Here's the config file I use for my personal website (this one):

postcss.config.js
const path = require("path");
const { grid } = require("./src/postcss/imported-mixins/grid");
const { button } = require("./src/postcss/imported-mixins/button");
const { guttery, gutterx } = require("./src/postcss/functions/gutter");
const { lineClamp } = require("./src/postcss/imported-mixins/lineClamp");

module.exports = {
  plugins: {
    "@csstools/postcss-global-data": {
      files: ["./src/postcss/customMedia.js"],
    },
    "postcss-preset-env": {
      autoprefixer: { grid: false },
      stage: 0,
      features: {
        clamp: false,
        "logical-properties-and-values": false,
        "media-query-ranges": {
          preserve: true,
        },
        "custom-properties": false,
      },
    },
    "postcss-mixins": {
      mixinsFiles: [
        path.join(__dirname, "src/postcss/{mixins/,mixins/**}"),
        path.join(__dirname, "src/css/{mixins/,mixins/**}"),
      ],
      mixins: {
        button,
        grid,
        lineClamp,
      },
    },
    "postcss-functions": {
      functions: {
        guttery,
        gutterx,
      },
    },
  },
};

Next.js includes a sensible default configuration that includes Autoprefixer. My config is 100% custom, when you opt for a custom config, Next.js completely disables theirs.

Warning: When you define a custom PostCSS configuration file, Next.js completely disables the default behavior. Be sure to manually configure all the features you need compiled, including Autoprefixer.

Customizing plugins

You'll notice quite a few things are configured, which I'll be covering through this guide:

  • PostCSS Preset Env
    • Autoprefixer
    • Stages
    • Features
  • PostCSS Mixins
  • PostCSS Functions

postcss preset env

Discovering postcss-preset-env was a game-changer for me. I first whet my appetite by using the ubiquitous Autoprefixer, which takes way the burden of adding vendor prefixes to CSS properties, like so:

/* before processing */
.element {
  transition: all 1s ease-out;
}

/* after processing */
.element {
  -webkit-transition: all 1s ease-out;
  -moz-transition: all 1s ease-out;
  -ms-transition: all 1s ease-out;
  -o-transition: all 1s ease-out;
  transition: all 1s ease-out;
}

While PostCSS serves as a bridge between CSS preprocessors like Sass and Less, PostCSS Preset Env take it up a notch by enabling modern CSS capabilities, while also providing polyfills for unsupported features.

Imagine being able to craft forward-compatible CSS without fretting over browser compatibility, thanks to PostCSS preset Env that's exactly what I can do!

PostCSS Preset Env lets you convert modern CSS into something most browsers can understand, determining the polyfills you need based on your targeted browsers or runtime environments

PostCSS Preset Env

So, instead of writing in Sass or Less, which are non-native, I've been writing CSS according to various draft CSS specifications. The progress of each specification is evaluated by what's known as a stage.

Here's a brief outline of these stages:

  • 0: Aspirational - Early stage, unstable, unofficial drafts open for discussion.
  • 1: Experimental - Recognized problem, unstable, no specific solution.
  • 2: Allowable - Specific solution proposed, still unstable.
  • 3: Embraced - Stable candidate recommendations, likely to become standard.
  • 4: Standardized - W3C recommendation, implemented by all browser vendors.
  • Rejected - Neglected or formally rejected specifications.

Typically, I opt for stage: 0.

While there's always a chance that certain features may be discarded or that specifications might alter, everything has been smooth sailing so far.

CSS Features that I use with postcss-preset-env

Take the following code, written in Sass:

$tablet-min: 768px;

.myClass {
  background: #b4d455;

  .thing {
    color: #b000b5;

    @media screen and (min-width: $tablet-min) {
      color: #907a70;
    }
  }
}

It works, but it isn't CSS, now take a look at how it can be re-written with CSS Nesting and Custom media, enabled with PostCSS Preset Env:

@custom-media --tablet-min (min-width: 768px);

.myClass {
  background: #b4d455;

  & .thing {
    color: #b000b5;

    @media screen and (--tablet-min) {
      color: #907a70;
    }
  }
}

There are some notable differences, mainly the & selector, in Sass you don't need it and after coming from Sass I originally found it confusing, but eventually got used to it.

I've been incorporating CSS Nesting for at-least 3 years as of the time of writing. It seems my gamble has paid off. The specification had little support in web browsers until 2023, but now more browser vendors are adding CSS nesting it's gaining traction and I won't have to rewrite the code I've been churning out, yay!

Without @custom-media, maintaining DRY (Don't Repeat Yourself) principles for media query values becomes a challenge, which was one of the benefits of using Sass / Less.

Defining custom media can be done in a couple of different ways. If you define them in JS then you'll have a single source of truth for both CSS and JS breakpoints.

customMedia.js
module.exports = {
  customMedia: {
    "--ipad-min": "(min-width: 768px)",
    "--ipad-max": "(max-width: 767px)",
  },
};

Bear in mind that you need to import either file in postcss.config.js.

I tend to avoid switching components with JS so more recently have opted for defining them in CSS, like this:

custom-media.css
@custom-media --ipad-min (min-width: 768px);
@custom-media --ipad-max (max-width: 767px);

PostCSS mixins

PostCSS mixins are another essential part of my setup. They enable me to create reusable chunks of CSS code, similar to what's possible with Sass or Less. This keeps my CSS organized and easier to maintain.

You can write useful little utilities such as the following clamp() mixin:

@define-mixin clamp $lines {
  text-overflow: ellipsis;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: $lines;
  -webkit-box-orient: vertical;
}

Which you would reference like so:

.productTitle {
  @mixin clamp 2;
}

Or a useful typography mixin like this one:

@define-mixin text $size {
  font-size: var(--font-size-$(size));
  line-height: var(--line-height-$(size));
}

You'd then reference the mixin in another file.

.heading {
  @mixin text lg;
}

You can go further and create mixins for buttons:

@define-mixin button $variant {
  font-family: var(--button-font-family);
  justify-self: start;
  display: grid;
  grid-column-gap: var(--button-column-gap);
  grid-auto-flow: column;
  align-items: center;
  align-content: center;
  justify-content: center;
  background: var(--button-$(variant)-bg);
  color: var(--button-$(variant)-color);
  appearance: none;
  font-size: var(--button-font-size);
  line-height: var(--button-line-height);
  border-radius: var(--button-border-radius);
  position: relative;
  text-decoration: none;
  padding: var(--button-padding);
  border: 1px solid;
  border-color: var(--button-$(variant)-border-color, transparent);
  white-space: nowrap;

  &:active,
  &.active {
    text-decoration: var(--button-$(variant)-active-text-decoration);
    color: var(--button-$(variant)-active-color);
    background: var(--button-$(variant)-active-bg);
    box-shadow: 0 0 0 2px var(--button-$(variant)-shadow-focus-color);
  }

  &:hover {
    text-decoration: none;
    color: var(--button-$(variant)-hover-color);
    background: var(--button-$(variant)-hover-bg);
    border-color: var(
      --button-$(variant)-hover-border-color,
      var(--button-$(variant)-border-color)
    );
  }

  &:focus {
    text-decoration: none;
    color: var(--button-$(variant)-focus-color);
    background: var(--button-$(variant)-focus-bg);
    border-color: var(
      --button-$(variant)-focus-border-color,
      var(--button-$(variant)-border-color)
    );
    box-shadow: 0 0 0 2px var(--button-$(variant)-focus-shadow-color);

    &.active {
      text-decoration: var(--button-$(variant)-active-text-decoration);
      color: var(--button-$(variant)-active-color);
      background: var(--button-$(variant)-active-bg);
      box-shadow: 0 0 0 2px var(--button-$(variant)-focus-shadow-color);
    }
  }
}

...which you would reference like:

.primary {
  @mixin button primary;
}

PostCSS functions

PostCSS Functions is a plugin for exposing Javascript functions to CSS. It's extremely useful for generating values, and is especially useful if you get tired of writing calc() css functions, or anything else that seems a little verbose and repetitive.

module.exports = {
  guttery(unit) {
    return `calc(var(--gutter-y) * ${unit})`;
  },
  gutterx(unit) {
    return `calc(var(--gutter-x) * ${unit})`;
  },
};
.element {
  display: grid;
  grid-row-gap: guttery(2);
  grid-column-gap: gutterx(1);
}

Conclusion

Understanding PostCSS and integrating it into my Next.js projects has allowed me to take advantage of modern CSS features, while still maintaining compatibility with older browsers, not only that, but I've streamlined my workflow and made my projects more maintainable.

If you haven't yet tried PostCSS, I highly recommend giving it a go and exploring the benefits.

If you have any suggestions as to how the examples can be made clearer, easier to understand, or how I can add any missing use-cases you feel would help others, I'd love nothing more than to hear from you! Reach out to me on Twitter, or shoot me an email.

Written by Morgan Feeney

I’ve been designing and developing for almost 2 decades.
Read about me, and my work.

For juicy tips and exclusive content, subscribe to my FREE newsletter.