Tailwind + Vue No-code editor

There has been much talk about no-code lately. This movement tries to approach software development to non-developers, offering tools that allow them to create and modify applications without using code. The benefits of no-code tools include speed, accessibility, reduced costs and autonomy.

Thinking about this idea, I wondered how to create a no-code editor for a web application. But, since a tool like this would be huge for a single post, I decided to focus only on the personalization of the styles and themes.

For that, I decided to rely on one of the most popular CSS frameworks at the moment: Tailwindcss. Not because of its usual use, but for all the tools it has in terms of configuration and CSS generation.

The idea is to create a frontend interface which allows to modify the Tailwindcss configuration on live and shows the result styles applied. Then, this customized configuration could be stored and used in the build and deployment process of a hypothetical application.

Architecture

But in this article, we are going to focus only on the editor part and how to preview on live the Tailwindcss config changes.

Architecture Detail

To achieve that, we are going to create a simple service in Node using ExpressJs. This service will receive the Tailwindcss configuration from the frontend editor and run Postcss with the Tailwindcss plugin to generate the CSS. Finally, the service will return the generated CSS to the editor, which will update the page to show the changes.

We could try to run the postcss and tailwind plugin directly in the browser, making it to work with node polyfills. This is what they do in play.tailwindcss.com using tailwind internals implementations. Another option is the brand new Web Containers but for simplicity’s sake, we do it in a simple node service.

Creating the project

Let’s create the new project called tailwind-editor with Vite running npm create. I’m going to use Vue for the Frontend because I’m more comfortable with it and also because it is Awesome ?

$ dev npm create vite@latest
✔ Project name: … tailwind-editor
✔ Select a framework: › Vue
✔ Select a variant: › JavaScript

Then, we add the dependencies for the service.

$ cd tailwind-editor
$ npm install --save express cors postcss tailwindcss

The resulting package:

{
  "name": "tailwind-editor",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.1",
    "postcss": "^8.4.17",
    "tailwindcss": "^3.1.8",
    "vue": "^3.2.37"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.1.0",
    "vite": "^3.1.0"
  }
}

Creating the Tailwindcss service

Now, we are going to create the service that will receive the Tailwindcss config and return the resulting CSS.

Let’s start with a file src/tailwind-as-a-service.js which will contain the ExpressJs server with the cors middleware to support cross-origin calls. It is listening by port 8080 to any request to the root path with a GET method and returns a text with Hello World.

// src/tailwind-as-a-service.js

import express from 'express';
import cors from 'cors';

const app = express()
app.use(cors());
const port = 8080

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Tailwind as a service listening on port ${port}`)
})

Running the server in node lets you check the response directly in the browser:

$ node ./src/tailwind-as-a-service.js

Hello World

Since we are using Vite and ES modules, the minimum node version required to follow this article is 14.18+.

So far, so good. Now we are going to configure postcss and its tailwind plugin to return CSS:

// src/tailwind-as-a-service.js
......
import postcss from 'postcss';
import tailwindcss from 'tailwindcss';

......

const defaultCss = `
  @import 'tailwindcss/base';
  @import 'tailwindcss/components';
  @import 'tailwindcss/utilities';
`;

app.get('/', async (req, res) => {
  const configuredTailwind = tailwindcss({
    content: [{ raw: '<div class="bg-red-500">', extension: 'html' }]
  });
  const postcssProcessor = postcss([configuredTailwind]);
  const { css } = await postcssProcessor.process(defaultCss);
  res.send(css);
});

......

We added the postcss and tailwindcss dependencies. Then we configure the Tailwindcss plugin for postcss with the content option.

This option tells Tailwindcss to inspect the HTML, JavaScript components and more files, to look for CSS classes to generate and include its CSS in the final result. It also allows us to put inline “raw” HTML.

After that, we create a postcssProcessor with the configured plugin of Tailwindcss. This is responsible for parsing the CSS and applying all postcss plugins.

Finally, we process a “fake” CSS file with the default base, components and utility styles of Tailwindcss. This is necessary to make Tailwindcss generate all necessary CSS.

The result CSS is returned in the response. So if we run again the service with node ./src/tailwind-as-a-service.js and we request from the browser, then we can see the resulting CSS:

CSS response

Here you can see the base CSS that Tailwindcss brings by default and also, at the end, the .bg-red-500 class that we are passing as raw HTML to the Tailwindcss config.

So we have a service to request and return the CSS, but how do we configure that CSS? well let’s make this service receive parameters and use them to configure the Tailwindcss plugin:

// src/tailwind-as-a-service.js
......

const defaultCss = `
  @import 'tailwindcss/base';
  @import 'tailwindcss/components';
  @import 'tailwindcss/utilities';
`;

app.post('/', async (req, res) => {
  const configuredTailwind = tailwindcss({
    content: [{ raw: req.body.html, extension: 'html' }],
    theme: req.body.theme
  });
  const postcssProcessor = postcss([configuredTailwind]);
  const { css } = await postcssProcessor.process(defaultCss);
  res.send(css);
});

......

Here, we changed the .get method into .post to be able to send and receive parameters in the request body, for larger parameters.

Moreover, we pick html and theme parameters from the request body and use them to configure Tailwindcss.

For simplicity, we are using only the theme part of the Tailwindcss configuration, but with this approach, we can configure any part of the Tailwindcss configuration.

Creating Editor

We are going to define some custom configuration for Tailwindcss Theme, which we will allow to modify by the user using an interface. We define here some values for a little example of components: buttons and titles. To keep small scope for this example we only allow to change of some colours and font properties:

// src/custom-tailwind-config.js

export const customTailwindConfig = {
  colors: {
    primary: {
      25: '#cdd3d6',
      50: '#243d48',
      75: '#1b2d36'
    },
    secondary: {
      25: '#bfe1ec',
      50: '#0086b2',
      75: '#006485'
    },
    success: {
      25: '#ecfdf5',
      50: '#10b981',
      75: '#065f46'
    },
    warning: {
      25: '#fffbeb',
      50: '#f59e0b',
      75: '#92400e'
    },
    error: {
      25: '#fef2f2',
      50: '#ef4444',
      75: '#991b1b'
    },
    title: {
      1: '#000',
      2: '#a3a3a3',
      3: '#000',
      4: '#0e7490'
    }
  },
  fontSize: {
    button: '1rem',
    'size-title1': '2rem',
    'size-title2': '1.5rem',
    'size-title3': '1.25rem',
    'size-title4': '1.125rem'
  },
  fontWeight: {
    'weight-button': '400',
    'weight-title1': '700',
    'weight-title2': '700',
    'weight-title3': '400',
    'weight-title4': '400'
  }
};

Keep in mind that we are using the Tailwind Theme configuration for simplicity’s sake and, it is not the only way to achieve the same result. The whole Tailwindcss configuration could be overridden, included the plugins used and/or their configuration. For example, you could create your own Tailwindcss plugin, adding all your CSS components based on a configuration passed to the plugin. This configuration could be passed as parameter to the Tailwindcss service as we are doing here with the theme configuration.

Now, we go to the App.vue component and remove all default content, adding some buttons and titles using the CSS utility classes which Tailwindcss generates with the previous Theme configuration:

// src/App.vue

<template>
  <section class="flex flex-col gap-10 min-w-[200px] m-10">
    <section class="flex flex-col gap-10">
      <button class="w-40 h-8 rounded bg-primary-50 hover:bg-primary-75 text-primary-25 hover:text-primary-25 text-button font-weight-button">
        Button Primary
      </button>
      <button class="w-40 h-8 rounded bg-secondary-50 hover:bg-secondary-75 text-secondary-25 hover:text-secondary-25 text-button font-weight-button">
        Button Secondary
      </button>
      <button class="w-40 h-8 rounded bg-success-50 hover:bg-success-75 text-success-25 hover:text-success-25 text-button font-weight-button">
        Button Success
      </button>
      <button class="w-40 h-8 rounded bg-warning-50 hover:bg-warning-75 text-warning-25 hover:text-warning-25 text-button font-weight-button">
        Button Warning
      </button>
      <button class="w-40 h-8 rounded bg-error-50 hover:bg-error-75 text-error-25 hover:text-error-25 text-button font-weight-button">
        Button Error
      </button>
    </section>
  
    <section class="flex flex-col gap-10 m-10">
      <h1 class="text-title-1 text-size-title1 font-weight-title1">
        Title 1
      </h1>
      <h2 class="text-title-2 text-size-title2 font-weight-title2">
        Tittle 2
      </h2>
      <h3 class="text-title-3 text-size-title3 font-weight-title3">
        Tittle 3
      </h3>
      <h4 class="text-title-4 text-size-title4 font-weight-title4">
        Tittle 4
      </h4>
    </section>
  </section>
  </section>
</template>

Before running the dev script to serve the Vue project, we will modify this script in our package.json to parallelise its execution with the tailwind service:

"dev": "node ./src/tailwind-as-a-service.js & vite",

Afterwards, we run the project and visit the locally served URL:

$ npm run dev

We are using the latest version of Vite, so the default port is 5153:

Running Vite

And finally, we open that URL in the browser and… Ups! no styles?! what’s going on??

First try

This is because we are not using Tailwindcss directly in our Vue project as we usually do. Instead, we need to call our tailwind-as-a-service endpoint to retrieve the CSS. Ok then, let’s create the function to call the service.

We create a new file fetch-css.js in the src directory:

// src/fetch-css.js

export async function fetchCss(tailwindCustomConfig) {
  return await fetch('http://localhost:8080', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      html: document.body.innerHTML,
      theme: {
        extend: tailwindCustomConfig
      }
    })
  }).then(response => response.text());
}

This async function is receiving the custom tailwind config and fetching the tailwind service passing it as theme:{ extend: tailwindCustomConfig } parameter. We pass it inside extendto keep all the default utilities as Tailwind has, and we only add the new ones we need. We also obtain all the current HTML on the page and send it to the service as html parameter. Tailwindcss will use this HTML to know which CSS classes generate and which don’t.

Following, we create a new component Editor.vue to use that function:

// src/components/Editor.vue

<script setup>
  import { onMounted, ref } from 'vue';
  import { customTailwindConfig } from '../custom-tailwind-config.js';
  import { fetchCss } from '../fetch-css.js';

  const css = ref('');

  async function getCss() {
    css.value = await fetchCss(customTailwindConfig);
  }

  onMounted(getCss);
</script>

<template>
  <component is="style">{{ css }}</component>
</template>

There are many things going on here:

  • We import the previous customTailwindConfig and the fetchCss function.
  • We add a css ref. If you are not familiar with the new Composition API of vue you can take a look at its documentation.
  • We create a new function getCss which calls the fetchCss and assigns the returned promise value to the ref value.
  • We use the onMounted Vue lifecycle hook to call the previous function whenever the component is mounted.
  • Finally, we create a dynamic component to attach the CSS to the DOM. This dynamic component will render the CSS inside a <style> tag, when the css ref is updated. This way whenever we update the css ref value, the styles will be updated.

We use a dynamic component as a workaround because the <style> tag is not allowed inside <template> tag by Vue template compiler.

Now, we import and use the Editor.vue component inside the App.vue:

// src/App.vue

<template>

  <div class="flex flex-row">
		......

    <Editor/>
  </div>
</template>

<script setup>
  import Editor from './components/Editor.vue';
</script>

If we reload again the URL then we do see the styles:

Second Try

Finally, the last step is to make the Editor.vue modify the default Theme from the Tailwindcss configuration and request again the CSS from the service to see a live view of the changes.

We are starting with the colours, adding a colour picker for each colour we want to configure.

// src/components/Editor.vue
<script setup>
  import { onMounted, reactive, ref, watch } from 'vue';
  ......

  const css = ref('');
  const editableCustomConfig = reactive(customTailwindConfig);

  async function getCss() {
    css.value = await fetchCss(editableCustomConfig);
  }

  onMounted(getCss);
  watch(editableCustomConfig, getCss);

</script>

<template>

  <div class="flex flex-col flex-nowrap gap-10 w-1/2  m-10">

    <h2 class="font-bold">Colors</h2>
    <section class="flex flex-row flex-wrap gap-10">

      <div v-for="(color, colorName) in editableCustomConfig.colors"
           style="display: flex; flex-flow: column nowrap;">
        <label v-for="(_, shadeName) in color">{{ colorName }} {{ shadeName }}
          <input type="color" v-model.lazy="color[shadeName]">
        </label>
      </div>

    </section>
  </div>

  <component is="style">{{ css }}</component>
</template>

In this step, we are doing several changes to be able to modify reactively the configuration:

  • In the <script>:

    • First, we create a reactive object with our customTailwindConfig as the initial value.
    • Then we fetch the CSS with this reactive object.
    • Finally, we add a watch to call getCss function, whenever this reactive object changes
  • In the <template>:

    • We add two loops v-for to iterate over each colour and each shade, binding a colour picker to the value.
    • We use the v-model directive with the lazy modifier to not request too many times whenever we move the selector on the colour picker.

    Notice we are binding the colour shade to the v-model using the colour and the shade name, instead of using the v-for variable directly. This is because the variable used to iterate in the v-for loops is not allowed to be modified. So the workaround is to access the value indirectly.

    Then, if we run again the application we can see the colour pickers and changing a color the component using that colour will be updated automatically:

    Colour Editor

Finally, we add also the part of the font size and font weight:

// src/components/Editor.vue
<script setup>
......
</script>

<template>
......

    <h2 class="font-bold">Font Sizes</h2>
    <section class="flex flex-row flex-wrap gap-10">
      <label v-for="(_, sizeName) in editableCustomConfig.fontSize">{{
          sizeName.replace('size-', '')
        }}
        <input type="number"
               step="0.125"
               class="w-14 border border-black text-center"
               :value="editableCustomConfig.fontSize[sizeName].replace('rem','')"
               @input="event=> editableCustomConfig.fontSize[sizeName] = event.target.value + 'rem'">rem
      </label>
    </section>

    <h2 class="font-bold">Font Weight</h2>
    <section class="flex flex-row flex-wrap gap-10">
      <label v-for="(_, weightName) in editableCustomConfig.fontWeight">{{
          weightName.replace('weight-', '')
        }}
        <input type="number"
               step="100"
               min="100"
               max="900"
               class="w-14 border border-black text-center"
               v-model="editableCustomConfig.fontWeight[weightName]">
      </label>
    </section>
  </div>

  <component is="style">{{ css }}</component>
</template>

Here we are repeating the same principle as with the colours, but with a single v-for for each case. Moreover, for the case of font size, we have to deal with the rem unit, adding and removing it, as it is necessary to pass it to the Tailwindcss configuration.

And here is the final aspect of the editor:

Full Editor

So this is the starting point to create your own no-code tool, to configure your project and see the changes on the fly. Remind that using the Theme values is not the only way to make this configurable, and you can use all the Tailwindcss config options. You can find all the working code here .Please, feel free to open issues, give feedback and, if you have found it useful, a star would be much appreciated ?.

Why not CSS custom properties AKA CSS variables?

We could achieve exactly the same by using CSS variables as values in the Tailwindcss Theme configuration, and just modifying the value of these variables in the front, without the need for any service neither Tailwindcss process on the fly.

Then, after saving these variables and loading them in production will have deployed the changes.

Why

Well, in this example we are only modifying the Theme part of the Tailwindcss configuration, but there are plenty more options in that configuration.

Imagine you created a Tailwindcss plugin which adds your own Design Components and CSS utilities. These plugins can have options too.

So, with the exposed solution you can modify all these possible configurations and options and see the result directly on the fly.

Also, you can use the created service to directly save the configuration on your database, by user or customer to later retrieve it during the deployment process or for any other purpose you need.

GitHub

View Github