DEV Community

Cover image for Integrating Preline UI in Flask Apps
Nicholas Ikiroma
Nicholas Ikiroma

Posted on • Updated on

Integrating Preline UI in Flask Apps

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(my favorite! šŸ˜) and Bootstrap, 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, 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.

You can code along or clone the project on Github: project repository

Set up Flask App

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

mkdir flask-preline
cd flask-preline
Enter fullscreen mode Exit fullscreen mode

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

virtualenv venv
source venv/bin/activate
Enter fullscreen mode Exit fullscreen mode

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:

pip install virtualenv
Enter fullscreen mode Exit fullscreen mode

Next, install Flask.

pip install flask
Enter fullscreen mode Exit fullscreen mode

Then, create a file app.py and add the code below.

from flask import Flask, render_template


app = Flask(__name__)


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


if __name__ == "__main__":
    app.run()
Enter fullscreen mode Exit fullscreen mode

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.

mkdir templates static
Enter fullscreen mode Exit fullscreen mode

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

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

flask-preline/
   - app.py
   - templates/
   - static/
Enter fullscreen mode Exit fullscreen mode

Next, install Tailwind using NPM

npm install -D tailwindcss
Enter fullscreen mode Exit fullscreen mode

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

npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

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.

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

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

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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:

npm install preline
Enter fullscreen mode Exit fullscreen mode

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

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

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

<script src="./node_modules/preline/dist/preline.js"></script>
Enter fullscreen mode Exit fullscreen mode

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:

<!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>
Enter fullscreen mode Exit fullscreen mode

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:

flask run --debug
Enter fullscreen mode Exit fullscreen mode

In the second terminal:

npx tailwindcss -i ./static/src/input.css -o ./static/dist/css/output.css --watch
Enter fullscreen mode Exit fullscreen mode

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

Screenshot of navigation bar

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

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 with the following lines of code:

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()
Enter fullscreen mode Exit fullscreen mode

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_directorytakes 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:

<script src="{{ url_for('serve_preline_js') }}"></script>
Enter fullscreen mode Exit fullscreen mode

Save your files and test the code again.

screenshot of request logs

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! šŸ˜€

Top comments (1)

Collapse
 
johnpetros profile image
JoĆ£o Pedro Carvalho

Thanks, you helped me a lot.