Integrating Preline UI in Flask Apps
Tags: flask, ui, webdev, python
Published: 2023-07-29 11:01:52

Writing CSS to build user interfaces can be fun for front-end developers, but it's a nightmare for most back-end developers. Thanks to frontend tools like [Tailwind CSS](https://tailwindcss.com/)(my favorite! 😍) and [Bootstrap](https://getbootstrap.com/), building UIs can be a more rapid and spontaneous process. Tailwind takes things up a notch with support for plugins that extend the capabilities of the framework.

In this article, you'll learn to configure [Preline UI](https://preline.co/index.html), a Tailwind plugin, in your Flask application. You'll discover how to integrate Tailwind in your Flask app and configure Tailwind to use Preline UI.

## Prerequisites

* Basic knowledge of Python and the Flask web framework
    
* Basic use of the command line
    
* PIP(package manager for Python) and NPM(package manager for Node)
    

## What is Tailwind CSS and How Does it Work?

Tailwind CSS is a utility-first CSS framework that empowers developers to quickly build user interfaces by using small utility classes that directly apply styles to HTML elements.

Think of it as having a collection of hundreds of pre-defined CSS classes that each serve specific purposes, such as creating buttons, adding text decoration, or adjusting margins and paddings.

Tailwind CSS offers these standardized classes, which can be easily combined in various ways, allowing for rapid and efficient web development.

## What is Preline UI?

Preline UI is an open-source set of prebuilt UI components based on the Tailwind CSS. The extension features over 300 UI component examples that are compatible with HTML, React, Vue, and others. By the end of this article, you'll know how to power your Flask app with Preline UI.

<mark>You can code along or clone the project on Github</mark>: [project repository](https://github.com/nicholasikiroma/flask-preline)

## Set up Flask App

First, you'll start by creating a project directory that you can name **flask-preline**.

```bash
mkdir flask-preline
cd flask-preline
```

Next, create a virtual environment and activate it. Virtual environments are a great way to isolate Python dependencies associated with a particular project.

```bash
virtualenv venv
source venv/bin/activate
```

The commands above create a virtual environment called **venv** and activate it. If you don't have `virtualenv` installed you'd run into an error.

Install `virtualenv` with the command below then follow the previous steps to create and activate your virtual environment:

```bash
pip install virtualenv
```

Next, install Flask.

```bash
pip install flask
```

Then, create a file [`app.py`](http://app.py) and add the code below.

```python
from flask import Flask, render_template


app = Flask(__name__)


@app.route('/')
def index():
    return render_template('index.html')


if __name__ == "__main__":
    app.run()
```

In the code above, `Flask` and `render_template` are imported from the `flask` package and a Flask object named `app` is created.

Using the Flask object, you define a view function called `index` that is triggered whenever you access the '/' route from your browser. The view function returns an HTML template using Flask's in-built `render_template` function.

## Configure Tailwind CSS

With the Flask app set up, you can configure the app to use Tailwind. To get started, create two folders in the project directory.

```bash
mkdir templates static
```

As the names imply, the folders would store HTML templates and static files respectively.

At this point, the project structure would look like this:

```text
flask-preline/
   - app.py
   - templates/
   - static/
```

Next, install Tailwind using NPM

```bash
npm install -D tailwindcss
```

Create a `tailwind.config.js` file with the command:

```bash
npx tailwindcss init
```

The previous command will create a Tailwind configuration file. Edit the config file with the code below to ensure Tailwind has access to your HTML templates.

```js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./templates/**/*.html",
    "./static/src/**/*.js"
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

Finally, create a new *static/src/* folder and add a new input.css file with the following content:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

With this in place, your Flask app is ready to use Tailwind.

## Set up Preline UI

Configuring the Preline UI plugin is also an easy process.

First, install Preline using NPM:

```bash
npm install preline
```

Then, include Preline UI in the `tailwind.config.js` file.

```js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./templates/**/*.html",
    "./static/src/**/*.js",
    'node_modules/preline/dist/*.js',
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('preline/plugin'),
  ],
}
```

Based on the Preline UI documentation, the next step is to include a Javascript `<script>` to make UI components interactive.

```html
<script src="./node_modules/preline/dist/preline.js"></script>
```

It gets tricky at this step when you're working with Flask. You'll see how this script behaves in your Flask app and a clever way to walk around the issue.

Before that, let's create an HTML template to test what we've done so far.

## HTML Templates

In the *templates* folder create a file *index.html* and add the following lines of code:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Flask and Preline UI</title>
    <link rel="stylesheet" href="{{ url_for('static',filename='dist/css/output.css') }}">
</head>
<body>
    <header class="flex flex-wrap sm:justify-start sm:flex-nowrap z-50 w-full bg-white border-b border-gray-200 text-sm py-3 sm:py-0 dark:bg-gray-800 dark:border-gray-700">
        <nav class="relative max-w-7xl w-full mx-auto px-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8" aria-label="Global">
          <div class="flex items-center justify-between">
            <a class="flex-none text-xl font-semibold dark:text-white" href="#" aria-label="Brand">Brand</a>
            <div class="sm:hidden">
              <button type="button" class="hs-collapse-toggle p-2 inline-flex justify-center items-center gap-2 rounded-md border font-medium bg-white text-gray-700 shadow-sm align-middle hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-white focus:ring-blue-600 transition-all text-sm dark:bg-slate-900 dark:hover:bg-slate-800 dark:border-gray-700 dark:text-gray-400 dark:hover:text-white dark:focus:ring-offset-gray-800" data-hs-collapse="#navbar-collapse-with-animation" aria-controls="navbar-collapse-with-animation" aria-label="Toggle navigation">
                <svg class="hs-collapse-open:hidden w-4 h-4" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
                  <path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
                </svg>
                <svg class="hs-collapse-open:block hidden w-4 h-4" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
                  <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
                </svg>
              </button>
            </div>
          </div>
          <div id="navbar-collapse-with-animation" class="hs-collapse hidden overflow-hidden transition-all duration-300 basis-full grow sm:block">
            <div class="flex flex-col gap-y-4 gap-x-0 mt-5 sm:flex-row sm:items-center sm:justify-end sm:gap-y-0 sm:gap-x-7 sm:mt-0 sm:pl-7">
              <a class="font-medium text-blue-600 sm:py-6 dark:text-blue-500" href="#" aria-current="page">Landing</a>
              <a class="font-medium text-gray-500 hover:text-gray-400 sm:py-6 dark:text-gray-400 dark:hover:text-gray-500" href="#">Account</a>
              <a class="font-medium text-gray-500 hover:text-gray-400 sm:py-6 dark:text-gray-400 dark:hover:text-gray-500" href="#">Work</a>
              <a class="font-medium text-gray-500 hover:text-gray-400 sm:py-6 dark:text-gray-400 dark:hover:text-gray-500" href="#">Blog</a>
      
              <div class="hs-dropdown [--strategy:static] sm:[--strategy:fixed] [--adaptive:none] sm:[--trigger:hover] sm:py-4">
                <button type="button" class="flex items-center w-full text-gray-500 hover:text-gray-400 font-medium dark:text-gray-400 dark:hover:text-gray-500 ">
                  Dropdown
                  <svg class="ml-2 w-2.5 h-2.5 text-gray-600" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
                    <path d="M2 5L8.16086 10.6869C8.35239 10.8637 8.64761 10.8637 8.83914 10.6869L15 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
                  </svg>
                </button>
      
                <div class="hs-dropdown-menu transition-[opacity,margin] duration-[0.1ms] sm:duration-[150ms] hs-dropdown-open:opacity-100 opacity-0 sm:w-48 hidden z-10 bg-white sm:shadow-md rounded-lg p-2 dark:bg-gray-800 sm:dark:border dark:border-gray-700 dark:divide-gray-700 before:absolute top-full sm:border before:-top-5 before:left-0 before:w-full before:h-5">
                  <a class="flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" href="#">
                    About
                  </a>
                  <div class="hs-dropdown relative [--strategy:static] sm:[--strategy:absolute] [--adaptive:none] sm:[--trigger:hover]">
                    <button type="button" class="w-full flex justify-between w-full items-center text-sm text-gray-800 rounded-md py-2 px-3 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300">
                      Sub Menu
                      <svg class="sm:-rotate-90 ml-2 w-2.5 h-2.5 text-gray-600" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M2 5L8.16086 10.6869C8.35239 10.8637 8.64761 10.8637 8.83914 10.6869L15 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
                      </svg>
                    </button>
      
                    <div class="hs-dropdown-menu transition-[opacity,margin] duration-[0.1ms] sm:duration-[150ms] hs-dropdown-open:opacity-100 opacity-0 sm:w-48 hidden z-10 sm:mt-2 bg-white sm:shadow-md rounded-lg p-2 dark:bg-gray-800 sm:dark:border dark:border-gray-700 dark:divide-gray-700 before:absolute sm:border before:-right-5 before:top-0 before:h-full before:w-5 top-0 right-full !mx-[10px]">
                      <a class="flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" href="#">
                        About
                      </a>
                      <a class="flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" href="#">
                        Downloads
                      </a>
                      <a class="flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" href="#">
                        Team Account
                      </a>
                    </div>
                  </div>
      
                  <a class="flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" href="#">
                    Downloads
                  </a>
                  <a class="flex items-center gap-x-3.5 py-2 px-3 rounded-md text-sm text-gray-800 hover:bg-gray-100 focus:ring-2 focus:ring-blue-500 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-gray-300" href="#">
                    Team Account
                  </a>
                </div>
              </div>
      
              <a class="flex items-center gap-x-2 font-medium text-gray-500 hover:text-blue-600 sm:border-l sm:border-gray-300 sm:my-6 sm:pl-6 dark:border-gray-700 dark:text-gray-400 dark:hover:text-blue-500" href="#">
                <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
                  <path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/>
                </svg>
                Log in
              </a>
            </div>
          </div>
        </nav>
    </header>
    <script src="../node_modules/preline/dist/preline.js"></script>
</body>
</html>
```

The HTML code above was gotten from the Preline UI website. It implements a responsive and interactive navigation bar. There are two aspects of the code that are worth highlighting.

* `<link rel="stylesheet" href="{{ url_for('static',filename='dist/css/output.css') }}">`: This part of the code uses Jinja2 templating engine to locate and access the CSS file that will be generated when you run Tailwind.
    
* `<script src="../node_modules/preline/dist/preline.js"></script>`: Here, a Javascript file located in the node\_modules is included in the HTML file. As mentioned, this script ensures the components from Preline UI are interactive.
    

## Test the Code

To test the code, you need to spin up your Flask server in one terminal. Then, in another terminal, run your Tailwindcss.

In the first terminal:

```bash
flask run --debug
```

In the second terminal:

```bash
npx tailwindcss -i ./static/src/input.css -o ./static/dist/css/output.css --watch
```

If everything was done right, you'd see the screen below when you access the route [http://127.0.0.1:5000](http://127.0.0.1:5000) on your browser.

![Screenshot of navigation bar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o9gazwdsm3mwhhbgsg8p.png align="left")

Going through the request logs you'd see a 404 error when Flask attempts to access the Javascript file from *node\_modules*.

![screenshot of request logs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ammorkvg0vkdu08afp0d.png align="left")

For some reason, Flask cannot access folders other than *static* or *templates*.

One way to solve this problem is to copy the Javascript file from *node\_modules* and paste it into the *static* folder which is accessible to the running Flask app. This approach would work, but it births another issue.

The `preline.js` file is updated whenever new UI components are added to your templates. Thus, new UI components added to your template will not be interactive. Hence, the need to manually update the file whenever changes are made to the template.

Thankfully, there's a better way to bypass this issue. Update [`app.py`](http://app.py) with the following lines of code:

```python
from flask import Flask, render_template, send_from_directory


app = Flask(__name__)


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/preline.js")
def serve_preline_js():
    return send_from_directory("node_modules/preline/dist", "preline.js")


if __name__ == "__main__":
    app.run()
```

In the code above, a view function named `send_from_directory` is defined. When the client makes a request to the */preline.js* route, the view function is triggered and the *preline.js* file is safely sent over to the client using Flask's `send_from_directory` function.

The send\_from\_directory function allows you to serve static files (e.g., JavaScript, CSS, images) directly from a specified directory on the server.

`send_from_directory`takes a directory path as the first argument. In this case, *preline.js* is located in "node\_modules/preline/dist". It means that Flask will look for the "preline.js" file in the "dist" folder within the "node\_modules/preline" directory. The second argument to send\_from\_directory is the name of the file to be served, which is "preline.js" in this case.

Finally, in the *index.html*, update the script tag with the following line:

```html
<script src="{{ url_for('serve_preline_js') }}"></script>
```

Save your files and test the code again.

![screenshot of request logs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ogiohfjhhfrcmu6ypv58.png align="left")

## Wrapping Up

UI components from Preline UI can indeed save you valuable time when building frontends for your applications, allowing you to focus on other critical aspects of your project. If you're familiar with other cool UI templates, help a struggling backend developer by sharing them in the comments. Happy Coding! :grinning: