DEV Community

Cover image for Deno Fresh PostCSS: Future CSS with Deno
Rodney Lab
Rodney Lab

Posted on • Originally published at

Deno Fresh PostCSS: Future CSS with Deno

πŸ›Έ Using PostCSS & Future CSS with Deno Fresh

In this post on Deno Fresh PostCSS, we take a look at setting up some vanilla CSS tooling for Deno Fresh. Deno Fresh is an SSR site builder. It has instant deploys with no build step. The absence of a build step sets Deno Fresh apart from other site builders like Astro or SvelteKit. Both of those process CSS, minify and handle some transformations for you. Although Deno Fresh lacks those features, the upside is that you have more control over your CSS; you make the decisions, not the tooling. That said, you need to do a little setup work to use popular tooling like PostCSS. That is what we look at here.

PostCSS tooling transforms input CSS. You can use it to generate prefixes automatically for legacy browsers or to minify the CSS. Minification just removes white-space and comments from CSS, optimizing the CSS for delivery to a browser. With our setup, we can keep the original CSS file (with comments and white-space). Each time we save it, under the hood, PostCSS will generate the shipped version. We do not just look at minification here. We also add some PostCSS processing to handle modern or future CSS, which is not yet fully integrated into the CSS Specification.

🧱 What are we Building?

Rather than build a project from scratch, we will just look at the setup we need to add to a Deno Fresh project to use PostCSS. We will see:

  • how you can configure PostCSS itself in Deno Fresh
  • how you can automatically generate processed CSS each time you save a CSS inputΒ file
  • a way to cache your static CSS assets with Deno Fresh

If that sounds good, then let’s get started.

βš™οΈ Deno Import Map

Let’s start by updating the import_map.json file in the project root directory:

  "imports": {
    "@/": "./",
    "$fresh/": "",
    "preact": "",
    "preact/": "",
    "preact-render-to-string": "*preact-render-to-string@5.2.4",
    "@preact/signals": "*@preact/signals@1.0.3",
    "@preact/signals-core": "*@preact/signals-core@1.0.1",
    "$std/": "",
    "postcss/": ""
Enter fullscreen mode Exit fullscreen mode

We added an import path alias in line 3 which we will use later. We also added the Deno Standard Library and a Deno PostCSS package.

πŸ’… Post CSS Config

The config itself will not be too different to what you are already familiar with from Node-based tooling. The main difference will be that we use an npm: prefix on imports. Create postcss.config.ts in the project root directory with the following content:

import autoprefixer from "npm:autoprefixer";
import csso from "npm:postcss-csso";
import customMediaPlugin from "npm:postcss-custom-media";
import postcssPresetEnv from "npm:postcss-preset-env";

export const config = {
  plugins: [
      stage: 3,
      features: {
        "nesting-rules": true,
        "custom-media-queries": true,
        "media-query-ranges": true,
Enter fullscreen mode Exit fullscreen mode

πŸ‘€ Watch Script

The last main piece is the watch script. This watches the CSS input folder for changes and runs PostCSS each time you save a file there. We will use a styles directory in the project root folder as the CSS input directory. When we save a CSS file there, the watch script will output the result to static/styles. static is for content which you want Deno Fresh to serve statically from your deployed project.

Create build-css.ts in the project root directory with the content below:

import { debounce } from "$std/async/mod.ts";
import { relative, resolve } from "$std/path/mod.ts";
import { config } from "@/postcss.config.ts";
import postcss from "postcss/mod.js";

const STYLES_INPUT_DIRECTORY = "styles";

async function buildStyles(path: string) {
  try {
    const css = await Deno.readTextFile(path);

    const { css: outputCss } = await postcss(config.plugins).process(css, {
      from: undefined,

    const __dirname = resolve();
    const outputPath = `./static/${relative(__dirname, path)}`;
    console.log(`Updating styles for ${outputPath}`);
    await Deno.writeTextFile(outputPath, outputCss);
  } catch (error: unknown) {
    console.error(`Error building styles for path ${path}: ${error as string}`);

const debouncedBuildStyles = debounce(async (path: string) => {
  await buildStyles(path);
}, 200);

const stylesOutputDirectory = `static/${STYLES_INPUT_DIRECTORY}`;
try {
} catch (error: unknown) {
  if (error instanceof Deno.errors.NotFound) {

const watcher = Deno.watchFs([`./${STYLES_INPUT_DIRECTORY}`]);
for await (const event of watcher) {
  const { paths } = event;
  paths.forEach((path) => {
Enter fullscreen mode Exit fullscreen mode
  • The buildStyles function (line 8) takes an updated styles path and processes it with PostCSS, saving the output to static/styles.
  • Calling debounceBuildStyles instead of buildStyles directly, stops the CSS building function from firing off too often. We limit it to run once at most in any 200Β ms period.
  • Lines 29 – 36 will run once when we start up the build CSS task (more about that in a moment) and create the output static/styles directory if it does not yet exist.
  • Lines 38 – 44 contain the watch code for file changes in the styles directory. If you added new files before firing up the build CSS task, just save them again once the task is running to make sure PostCSS processes them.

πŸ’„ Build CSS Task

Next, we can create a Deno task to run the watch script created in the last section. Update the deno.json file in the project root directory to add a build:cssΒ task:

  "tasks": {
    "build:css": "deno run --allow-env=DENO_ENV --allow-read --allow-write build-css.ts",
    "start": "deno run -A --watch=static/,routes/ dev.ts"
  "importMap": "./import_map.json",
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
Enter fullscreen mode Exit fullscreen mode

We give the task just the permissions it needs here. These are read-write access and access to the DENO_ENV environment variable. You can be more restrictive if you prefer, listing the specific directories Deno will apply the read and write permissions to.

You can open a new Terminal tab and run this new task there:

mkdir styles # create a `styles` directory if you don't yet have one
deno task build:css
Enter fullscreen mode Exit fullscreen mode

Then in your previous Terminal tab, start up the Deno Fresh server (as usual) by running deno task start. Try creating a new CSS file at styles/global.css for example. If you open up static/styles/global.css you should see a minified version of this CSS which PostCSS has generated.

πŸ’― Deno Fresh PostCSS: Testing

Add a few more CSS files and try using new features like range media queries. You can include the CSS in a rel tag in one of your Preact markup files:

import { asset, Head } from "$fresh/runtime.ts";

export default function Home() {
  return (
        <link rel="stylesheet" href={asset("/styles/fonts.css")} />
        <link rel="stylesheet" href={asset("/styles/global.css")} />
                <!-- TRUNCATED... -->
      <main className="wrapper">
        <BannerImage />
        <h1 className="heading">FRESH</h1>
        <p className="feature">Fresh πŸ‹ new framework!</p>
        <Video />
Enter fullscreen mode Exit fullscreen mode

Here I wrapped the href for the stylesheets in the asset function. This is helpful for cache busting. Deno Fresh will hash the content of the files and append the calculated hash to the filename. When the CSS changes, the hash and consequently file name change, so no outdated CSS cached in-browser or in a CDN will be applied.

Take a look in static/styles. Your file should contain minified CSS and look something like this:

:root{--colour-dark:hsl(242deg 94% 7%);--colour-light:hsl(260deg 43% 97%);--colour-brand:hsl(50deg 100% 56%);--colour-theme:hsl(204deg 69% 50%);--colour-alt:hsl(100deg 100% 33%);--spacing-0:0;--spacing-1:0.25rem; /* TRUNCATED...*/
Enter fullscreen mode Exit fullscreen mode

πŸ™ŒπŸ½ Deno Fresh PostCSS: Wrapping Up

We had an introduction to how you can set up PostCSS with Deno Fresh. In particular, you saw:

  • Deno code for triggering a function when a file changes
  • how to be selective in Deno task permissions
  • some Deno file APIs

The complete code for this project is in the Rodney Lab GitHub repo. I do hope the post has either helped you with an existing project or provided some inspiration for a new one. As an extension, you can add all your favourite future CSS rules to the PostCSS config. Beyond PostCSS for linting your input CSS, consider trying stylelint.

Get in touch if you have some questions about this content or suggestions for improvements. You can add a comment below if you prefer. Also, reach out with ideas for fresh content on Deno or other topics!

πŸ™πŸ½ Deno Fresh PostCSS: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, then please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter, on Mastodon and also the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as Deno. Also, subscribe to the newsletter to keep up-to-date with our latest projects.

Top comments (0)