The Flask Mega-Tutorial Part XI: Facelift

This is the eleventh installment of the Flask Mega-Tutorial series, in which I'm going to tell you how to replace the basic HTML templates with a new set that is based on the Bootstrap user interface framework.

For your reference, below is a list of the articles in this series.

You have been playing with my Microblog application for a while now, so I'm sure you noticed that I haven't spent too much time making it look good, or better said, I haven't spent any time on that. The templates that I put together are pretty basic, with absolutely no custom styling. It was useful for me to concentrate on the actual logic of the application without having the distraction of also writing good looking HTML and CSS.

But I've focused on the backend part of this application for a while now. So in this chapter I'm taking a break from that and will spend some time showing you what can be done to make the application look a bit more polished and professional.

This chapter is going to be a bit different than previous ones, because I'm not going to be as detailed as I normally am with the Python side, which after all, is the main topic of this tutorial. Creating good looking web pages is a vast topic that is largely unrelated to Python web development, but I will discuss some basic guidelines and ideas on how to approach the task, and you will also have the application with the redesigned looks to study and learn from.

The GitHub links for this chapter are: Browse, Zip, Diff.

CSS Frameworks

While we can argue that coding is hard, our pains are nothing compared to those of web designers, who have to write templates that have a nice and consistent look on a list of web browsers. It has gotten better in recent years, but there are still obscure bugs or quirks in some browsers that make the task of designing web pages that look nice everywhere very hard. This is even harder if you also need to target resource and screen limited browsers of tablets and smartphones.

If you, like me, are a developer who just wants to create decent looking web pages, but do not have the time or interest to learn the low level mechanisms to achieve this effectively by writing raw HTML and CSS, then the only practical solution is to use a CSS framework to simplify the task. You will be losing some creative freedom by taking this path, but on the other side, your web pages will look good in all browsers without a lot of effort. A CSS framework provides a collection of high-level CSS classes with pre-made styles for common types of user interface elements. Most of these frameworks also provide JavaScript add-ons for things that cannot be done strictly with HTML and CSS.

Introducing Bootstrap

One of the most popular CSS frameworks is Bootstrap, created by Twitter. If you want to see the kind of pages that can be designed with this framework, the documentation has some examples.

These are some of the benefits of using Bootstrap to style your web pages:

  • Similar look in all major web browsers
  • Handling of desktop, tablet and phone screen sizes
  • Customizable layouts
  • Nicely styled navigation bars, forms, buttons, alerts, popups, etc.

The most direct way to use Bootstrap is to simply import the bootstrap.min.css file in your base template. You can either download a copy of this file and add it to your project, or import it directly from a CDN. Then you can start using the general purpose CSS classes it provides, according to the documentation, which is pretty good. You may also want to import the bootstrap.min.js file containing the framework's JavaScript code, so that you can also use the most advanced features.

There is a Flask extension called Flask-Bootstrap that provides a ready to use base template that has the Bootstrap framework installed. I should note that the Flask-Bootstrap extension uses Bootstrap version 3, which is not the latest version of this project. Unfortunately the author of this extension never upgraded it to support newer versions. There are a few forks on GitHub, but as of July 2021 none of them have support for Bootstrap 5, which is the current version. I have decided to continue using the Flask-Bootstrap extension because it is quite good and continues to work fine with Bootstrap 3.

Let's install this extension:

(venv) $ pip install flask-bootstrap

Using Flask-Bootstrap

Flask-Bootstrap needs to be initialized like most other Flask extensions:

app/__init__.py: Flask-Bootstrap instance.

# ...
from flask_bootstrap import Bootstrap

app = Flask(__name__)
# ...
bootstrap = Bootstrap(app)

With the extension initialized, a bootstrap/base.html template becomes available, and can be referenced from application templates with the extends clause.

But as you recall, I'm already using the extends clause with my own base template, which allows me to have the common parts of the page in a single place. My base.html template defined the navigation bar, which included a few links, and also exported a content block . All other templates in my application inherit from the base template and provide the content block with the main content of the page.

So how can I fit the Bootstrap base template? The idea is to use a three-level hierarchy instead of just two. The bootstrap/base.html template provides the basic structure of the page, which includes the Bootstrap framework files. This template exports a few blocks for derived templates such as title, navbar and content (see the complete list of blocks here). I'm going to change my base.html template to derive from bootstrap/base.html and provide implementations for the title, navbar and content blocks. In turn, base.html will export its own app_content block for its derived templates to define the page content.

Below you can see how the base.html looks after I modified it to inherit from the Bootstrap base template. Note that this listing does not include the entire HTML for the navigation bar, but you can see the full implementation on GitHub or by downloading the code for this chapter.

app/templates/base.html: Redesigned base template.

{% extends 'bootstrap/base.html' %}

{% block title %}
    {% if title %}{{ title }} - Microblog{% else %}Welcome to Microblog{% endif %}
{% endblock %}

{% block navbar %}
    <nav class="navbar navbar-default">
        ... navigation bar here (see complete code on GitHub) ...
{% endblock %}

{% block content %}
    <div class="container">
        {% with messages = get_flashed_messages() %}
        {% if messages %}
            {% for message in messages %}
            <div class="alert alert-info" role="alert">{{ message }}</div>
            {% endfor %}
        {% endif %}
        {% endwith %}

        {# application content needs to be provided in the app_content block #}
        {% block app_content %}{% endblock %}
{% endblock %}

Here you can see how I make this template derive from bootstrap/base.html, followed by the three blocks that implement the page title, navigation bar and page content respectively.

The title block needs to define the text that will be used for the page title, with the <title> tags. For this block I simply moved the logic that was inside the <title> tag in the original base template.

The navbar block is an optional block that can be used to define a navigation bar. For this block, I adapted the example in the Bootstrap navigation bar documentation so that it includes a site branding on the left end, followed by the Home and Explore links. I then added the Profile and Login or Logout links aligned with the right border of the page. As I mentioned above, I omitted the HTML in the example above, but you can obtain the full base.html template from the download package for this chapter.

Finally, in the content block I'm defining a top-level container, and inside it I have the logic that renders flashed messages, which are now going to appear styled as Bootstrap alerts. That is followed with a new app_content block that is defined just so that derived templates can define their own content.

The original version of all the page templates defined their content in a block named content. As you saw above, the block named content is used by Flask-Bootstrap, so I renamed my content block as app_content. So all my templates have to be renamed to use app_content as their content block. As an example, here how the modified version of the 404.html template looks like:

app/templates/404.html: Redesigned 404 error template.

{% extends "base.html" %}

{% block app_content %}
    <h1>File Not Found</h1>
    <p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}

Rendering Bootstrap Forms

An area where Flask-Bootstrap does a fantastic job is in rendering of forms. Instead of having to style the form fields one by one, Flask-Bootstrap comes with a macro that accepts a Flask-WTF form object as an argument and renders the complete form using Bootstrap styles.

Below you can see the redesigned register.html template as an example:

app/templates/register.html: User registration template.

{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}

{% block app_content %}
    <div class="row">
        <div class="col-md-4">
            {{ wtf.quick_form(form) }}
{% endblock %}

Isn't this great? The import statement near the top works similarly to a Python import on the template side. That adds a wtf.quick_form() macro that in a single line of code renders the complete form, including support for display validation errors, and all styled as appropriate for the Bootstrap framework.

Once again, I'm not going to show you all the changes that I've done for the other forms in the application, but these changes are all made in the code that you can download or inspect on GitHub.

Rendering of Blog Posts

The presentation logic that renders a single blog posts was abstracted into a sub-template called _post.html. All I need to do with this template is make some minor adjustments so that it looks good under Bootstrap.

app/templates/_post.html: Redesigned post sub-template.

    <table class="table table-hover">
            <td width="70px">
                <a href="{{ url_for('user', username=post.author.username) }}">
                    <img src="{{ post.author.avatar(70) }}" />
                <a href="{{ url_for('user', username=post.author.username) }}">
                    {{ post.author.username }}
                {{ post.body }}

Rendering Pagination Links

Pagination links is another area where Bootstrap provides direct support. For this I just went one more time to the Bootstrap documentation and adapted one of their examples. Here is how these look in the index.html page:

app/templates/index.html: Redesigned pagination links.

    <nav aria-label="...">
        <ul class="pager">
            <li class="previous{% if not prev_url %} disabled{% endif %}">
                <a href="{{ prev_url or '#' }}">
                    <span aria-hidden="true">&larr;</span> Newer posts
            <li class="next{% if not next_url %} disabled{% endif %}">
                <a href="{{ next_url or '#' }}">
                    Older posts <span aria-hidden="true">&rarr;</span>

Note that in this implementation, instead of hiding the next or previous link when that direction does not have any more content, I'm applying a disabled state, which will make the link appear grayed out.

I'm not going to show it here, but a similar change needs to be applied to user.html. The download package for this chapter includes these changes.

Before And After

To update your application with these changes, please download the zip file for this chapter and update your templates accordingly.

Below you can see a few before and after pictures to see the transformation. Keep in mind that this change was achieved without changing a single line of application logic!

Login Home Page


  • #26 Miguel Grinberg said 2018-05-03T19:00:47Z

    @Sam: sure, why not. You can modify the HTML templates and add any elements or styles that you like.

  • #27 kk said 2018-06-02T02:08:57Z

    what's difference bootstrap and flask-bootstrap?could I use it together?

  • #28 Miguel Grinberg said 2018-06-02T05:18:23Z

    @kk: bootstrap is a CSS/JavaScript framework for the browser. Flask-Bootstrap is a Flask extension that makes bootstrap easier to integrate with Flask apps.

  • #29 Esther said 2018-06-25T17:47:01Z

    Hi Miguel! Thank you so much for your tutorial, I've learned a ton from it so far.

    I am receiving error msgs after implementing the code for navbar from your github (lines 7-52):

    {% block navbar %} ... {% endblock %}

    Error msg: BuildError: Could not build url for endpoint 'main.index'. Did you mean 'index' instead?

    Thanks, E.

  • #30 Miguel Grinberg said 2018-06-27T15:05:16Z

    @Esther: You are looking at a more advanced version of the project. When you access GitHub, use the links provided at the top of this article, so that you access the files as they are in this chapter.

  • #31 Julian said 2018-07-09T01:39:36Z

    Hi Miguel, I was just reading trough this chapter and I realized that flask-bootstrap is using bootstrap v3 (right?) and since bootstrap v4.1 is already out and stable (I think) I was wondering if there's any reliable option to use something similar as flask-bootstrap (so I can still use the fast wtf implementation) but using bootstrap 4 and if there's any advantage in using the v4 rather than the older v3, thanks.

  • #32 Miguel Grinberg said 2018-07-09T16:36:57Z

    @Julian: There is a fork of Flask-Bootstrap that implements the v4 API called Flask-Bootstrap4. I can't comment on it as I haven't used it myself. The author of Flask-Bootstrap intends to add support for v4 as well, but my understanding is that he currently lacks the time to do this work.

  • #33 Johannes said 2018-07-31T15:02:53Z

    Hi Miguel,

    Forgive me if this is a dumb question, I'm following the tutorial but I have a hard time graspin how to make wtf.quick_form() display errors in a similar way to before the "Facelift", submitting without data only redirects back to the index page, displaying no errors. Is there something major I am missing here?

  • #34 Miguel Grinberg said 2018-07-31T21:15:39Z

    @Johannes: the quick_form() call is a Jinja2 macro that generates the HTML that renders the fields and the validation errors. If you are not seeing validation errors then check that your form and your validators are correctly defined. You may want to try this with my version of the code from GitHub, to rule out any mistakes you may have made.

  • #35 Warsenius said 2018-08-12T08:46:14Z

    Hi Miguel.

    I thought with bootstrap you can select a style\theme for your whole site? That you set a link to a CDN url for what theme you want. How do you do that using flask-bootstrap.

    Also, at the Pagination documentation from getbootstrap i see they use a example where they show the numbers for all the pages, so you can quickly go to a specific page and see how many pages there are. However you need some logic so the site knows how many pages there are (based on # of post in the db) and so each number forwards to the corresponding page. How would this be done?

    I find pagination that do not provide this very frustrating to use(like on the comment section of this blog).

  • #36 Miguel Grinberg said 2018-08-13T14:26:54Z

    @Warsenius: if you want to use a theme you can just include it in the base template in the proper place. For the pagination control I'm not really sure what you are asking. You as the developer can generate your pagination buttons in any of the styles, I chose the simpler buttons, but you can ignore that and write your templates to use the more complex pagination if that is what you like.

  • #37 Warsenius said 2018-08-13T17:23:09Z

    Hello Miguel. Thanks for your reply. Regarding the pagination question. in the documentation it says: 1 2 3 4 5

    Since the amount of pages we have in the blog is variable depending on the amount of posts we cant define them staticly. You want the amount of links to be dependent on the amount of pages and then maxing at certain amount. and have the links dynamicly go to the right page number. like this example: https://cloud.netlifyusercontent.com/assets/344dbf88-fdf9-42bb-adb4-46f01eedd629/3fa3cbec-3577-475f-afe2-68f567730031/misal.gif

  • #38 Miguel Grinberg said 2018-08-13T22:08:18Z

    @Warsenius: the pagination control from bootstrap is not really smart, all it does is provide you with the markup to create the links. It is up to the application to decide how many links to create. Many application use an ellipsis to keep the number of links from growing indefinitely. For example, you could have a maximum of 7 links with the following structure: First Page | ... | Page Current-1 | Page Current | Page Current + 1 | ... | Last Page. The ellipsis are rendered as static, so that they are not clickable.

  • #39 John Mann said 2018-08-26T11:55:53Z

    Any tips or guidance on adding Sass or SCSS to a Flask application? I've seen several options and was wondering what you would recommend. Thanks.

  • #40 Miguel Grinberg said 2018-08-26T13:55:55Z

    @John: if you have a full blown javascript application that also includes a CSS pre-processor, then I recommend that you create your JS application using the tools provided by the framework that you use. If all you need is to process a CSS file, then installing the CSS compiler tool and then writing either a makefile or even a simple script that runs the compiler should suffice.

  • #41 Derek said 2018-09-20T09:59:41Z

    Hi Miguel,

    I am new to web dev and this tutorial is fantastic, thank you. I am working through the new paid version on your website, and following the pdf as well. I have hit a problem in chapter 11 Facelift. After implementing changes my microblog does not show the Bootstrap enhanced version and there are no errors. Status fo far: - Using the github code v0.11 as is, without alteration (Actually tried 12 and 13 as well) - Double checked to ensure all bootstrap code is in checked out version. - flask-bootstrap installed under venv without error and is reporting ver in pip list - Have run app in flask debug mode and without - neither report error. - The site works perfectly as appropriate to stage 11 but everything renders as the "before" version. - Been using Chrome to test and forced the refresh - switched to safari and firefox - same result. - The only difference between the instructions and my implementation is that I run the server on an Ubuntu 18.04.1 vm using the flask run -h option.

    As mentioned above, I am a web dev newbie and just not sure where to look next? Thanks again Miguel

  • #42 Aleksandr said 2018-09-20T11:00:53Z

    What if you want to make submit button deactivated if there are no changes in user profile? How could we add any logic to that button?

  • #43 Miguel Grinberg said 2018-09-20T22:36:49Z

    @Derek: this is really strange, I cannot explain it. Either you have the old version of the application running on your machine and you think you are using the new version but you are using still the old one, or there is some sort of caching issue. But if you tried multiple browsers, that leaves #1 as the only possibility. Restart your computer, double check that you have the new code and try again.

  • #44 Miguel Grinberg said 2018-09-20T22:37:39Z

    @Aleksandr: this is done via JavaScript. Consult the bootstrap documentation to learn what style needs to be set in the button to make it disabled or enabled.

  • #45 Senrya said 2018-09-24T04:44:03Z

    Hi Miguel, thank you so much for your wonderful tutorial again. I have a small issue here, can I ask for your advice? "... Home Explore Plot ... " As you can see, I added an additional link which is "plot" under the "explore" link, and I created a route for that in routes.py. However, I got this result: "... raise BuildError(endpoint, values, method, self) werkzeug.routing.BuildError: Could not build url for endpoint 'plot'. Did you mean 'logout' instead? " What can I do to add more links or buttons on the navigation bar besides Home, Explore, Log In, Profile, Log Out?

  • #46 Miguel Grinberg said 2018-09-25T18:08:09Z

    @Senrya: the error is telling you that there is no view function named "plot". Did you name the new view function differently? Use the name of the function in the url_for call.

  • #47 Senrya said 2018-09-28T08:08:04Z

    @Miguel: Thank you, I fixed it. I though the url_for was meant for the "/url", not the name of the function. Silly me!

  • #48 Noob42 said 2018-10-08T12:54:11Z

    Hi, How do we replace the bootstrap.min.css ?

  • #49 Miguel Grinberg said 2018-10-09T09:42:57Z

    @Noob42: You can replace it by rewriting the "styles" block, but if you want to use a different major version of Bootstrap, then you should not use this extension and instead work with Bootstrap directly.

  • #50 Oleg Kovalenko said 2018-10-12T10:54:34Z

    It is worth pointing out that Flask-Bootstrap is un-maintained, and appears (at a glance) not to be compatible with Bootstrap 4. There is a project called Bootstrap-Flask that is maintained and up to date for Bootstrap 4. Additionally, Bootstrap-Flask provides some clean macros that would remove most of the Bootstrap specific code in this chapter.

    @Miguel Are you open to receiving a pull request to move the code to Bootstrap-Flask? It would be beautifully suited to your tutorial, resulting in a tighter focus on Flask.

Leave a Comment