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.

Note 1: If you are looking for the legacy version of this tutorial, it's here.

Note 2: If you would like to support my work on this blog, or just don't have patience to wait for weekly articles, I am offering the complete version of this tutorial packaged as an ebook or a set of videos. For more information, visit courses.miguelgrinberg.com.

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.

Fortunately, there is a Flask extension called Flask-Bootstrap that provides a ready to use base template that has the Bootstrap framework installed. 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


  • #51 Miguel Grinberg said 2018-10-12T22:46:41Z

    @Oleg: The project that you referenced is less than four months old. I prefer to stay with a package that is stable, even if that means using an older version of Bootstrap. In any case, the goal is to teach you how to use these packages in general, you should use any of the forks that have Bootstrap 4 support if that is what you want for your project.

  • #52 Oleg Kovalenko said 2018-10-16T14:14:13Z

    For anybody that wants to add aria accessibility fields to Bootstrap forms rendered using FlaskWTF, you can do something like {{ form.field(class='form-control', placeholder="...", **{"aria-label":"search", "aria-describedby":"searchButton") }}. Those won't work as kwargs because the hyphens break variable naming rules in Python.

    I'm reworking my Microblog to use Bootstrap 4 and I'm pulling a lot of accessibility minded markup from the Bootstrap documentation, so I'm actually passing all arguments using the form.field(**{dict}) syntax.

  • #53 Bogdan said 2018-12-18T12:13:30Z

    Hello Miguel, Wonderful tutorial! One can learn so many concepts and how-to's from it!

    I have a problem with bootstrap: By default it seems bootstrap searches for files in the location below [18/Dec/2018 13:50:50] "GET /static/bootstrap/css/bootstrap.min.css.map HTTP/1.1" 404 - but I found the files to be in: /venv/Lib/site-packages/flask_bootstrap/static/css

    Can I change this location?

  • #54 Miguel Grinberg said 2018-12-18T12:36:20Z

    @Bogdan: by default Flask-Bootstrap loads the css and js files from a CDN. If you request the files to be served from the static location, then Flask-Bootstrap configures the static route in its blueprint so that the /static URL maps to the location where the files were installed. In either case, you don't need to do anything.

  • #55 joaquin said 2019-01-14T01:54:57Z

    Do you plan to ever support bootstrap 4? Flask-bootstap is currently set at bootstrap 3, and the repo seems to be dead. I installed bootstrap4 manually, but using the code in the examples it looks weird.

  • #56 Miguel Grinberg said 2019-01-14T13:30:21Z

    @joaquin: the concepts used in this article apply to Bootstrap4. You can use the Flask-Bootstrap4 extension in the same way with the updated styles from that version of the framework. Eventually I may upgrade this article to use that version, but not planning to do it in the short term, none of the bootstrap4 extensions for Flask is as stable as the version 3.

  • #57 Rimu said 2019-02-19T03:28:01Z

    I just swapped flask-bootstrap for flask-bootstrap4 and it was fairly painless - it looks like they have nearly identical python code with just the css and js upgraded.

    I needed to change a lot of classes in the nav and some on the pagination but it was straightforward.

  • #58 Kelvin said 2019-04-08T12:21:43Z

    Good tutorial

  • #59 Jeremy said 2019-04-23T14:34:35Z

    Hi Miguel, Thank you for your work on this tutorial. I'm trying to learn how to change blue background color of the application. It seems; it's just not as easy as editing text editor? I have tried to save and reload. Would you point me in the right direction to learn more about how to do this?

  • #60 Miguel Grinberg said 2019-04-23T21:52:23Z

    @Jeremy: that needs to be done with CSS. What are you editing to change the background?

  • #61 Inigo Montoya said 2019-05-27T21:10:13Z

    Hey Miguel! I started programming a short while ago, and I still have some trouble understanding the way this whole thing works.. once I write "flask run" on the command line, what is happening to the code? I mean, there are a few files in this program... what's the hierarchy? Does one file "summon" the other? and which one is the first to be summoned if so..?

    And another thing- what is the part of every kind of file? Are all the files ending with .py serve the same purpose as the ones ending with .html? Im aware that they're not the same type of file, but when do you use each sort of file?

  • #62 Miguel Grinberg said 2019-05-27T22:00:30Z

    @Inigo: the flask run command will run the file that is set in the FLASK_APP environment variable. Other files in your application are imported from this file. HTML files are used to display content in the browser, while PY files have Python code that runs in the server.

  • #63 Randy Pittman said 2019-07-06T18:34:01Z

    Up until now I've felt like I more-or-less got what flask was doing in the background, but all of a sudden I feel way out of my depth with bootstrap. Can you recommend any resources to grok bootstrap and flask-bootstrap? It feels like I need to stop here and go through a "Bootstrap and flask-bootstrap Mega Tutorial" before I'll be able to use it well. Am I overreacting?

  • #64 Miguel Grinberg said 2019-07-06T19:19:46Z

    @Randy: yes, you may be overreacting just a bit. :)

    Flask-Bootstrap doesn't really do much, just make Bootstrap more accessible to Flask applications. Bootstrap itself is a large framework, but it has good documentation that is very copy-pasteable. If you understand the basics of HTML and CSS you should have no problem looking for the things that you are interested in adding to your site in their documentation, copy/pasting it, and then adapting it to your needs.

  • #65 vida said 2019-09-13T11:37:16Z

    Thanks for a great tutorial. Can I please ask for some help? All was going fine until this chapter... then installing flask-bootstrap has broken the app.. I am using conda. So i had to do "conda install -c conda-forge flask-migrate"

    And I made the code changes as above... But now when I try to run I'm getting errors to do with versioning and Jinja 2.10

    File "/anaconda3/envs/megatute35/lib/python3.5/site-packages/pkg_resources/init.py", line 786, in resolve raise VersionConflict(dist, req).with_context(dependent_req) pkg_resources.ContextualVersionConflict: (Jinja2 2.10 (/anaconda3/envs/megatute35/lib/python3.5/site-packages), Requirement.parse('Jinja2>=2.10.1'), {'Flask'})

    I'm just not sure what to do to resolve this? Any advice would be great...

  • #66 Miguel Grinberg said 2019-09-13T12:19:55Z

    @vida: have you tried creating a brand new virtualenv and adding all your dependencies again?

  • #67 vida said 2019-09-16T13:52:33Z

    Thanks Michael... I am in the process of doing that now.. I think the problem is related to the version of flask-bootstrap on conda forge.. must be out of date. I'm using conda environments so when I can get to a position to try it I'm going to take the un-recommended step of seeing if I can pip install it in the conda env ... Thanks

  • #68 vida said 2019-09-16T14:00:44Z

    Just FYI and for anyone who might be in same situation. I am using Conda and PyCharm.. and the problem I had was to do with the use of the package manager in Pycharm.. it is a little bit confusing at times, you can end up with the wrong repository. So I had really old versions of some packages. The thing to do is be very careful when adding packages in the UI. Make sure you have selected "Use Conda Package Manager" when you are working with packages in the Preferences/Project Interpreter.

    Also in the Configuration to run the project.. make sure you change the Python Interpreter selected. I had was driving myself crazy cos I made a fresh new Interpreter with all new packages installed in it.. but kept seeing the same errors.. this was because I forgot to select the new interpreter in the Run Configuration.

    Hope this helps somebody.

  • #69 Miguel Angelo Rozsas said 2019-09-20T19:37:06Z

    Hi !

    Sorry if I missed it, but how do I switch the app to use another bootstrap framework, like this one: Narrow jumbotron from bootstrap examples page ?

    As far I understand so no, Flask-Bootstrap loads a basic template or look, is that correct ? If so, to load another bootstrap framework I should not use Flask-Bootstrap but, instead, load some CSS by my self ?

    I appreciate any clarification you can provide about this subject...

    PS: I am not a web developer, so, theses things are a bit confusing to me...

  • #70 Miguel Grinberg said 2019-09-21T07:17:23Z

    @Miguel: what do you mean by "another" bootstrap framework? Maybe you are trying to load a theme on top of the framework? This should be done according to the instructions on the theme, you'll probably need to include an additional CSS file manually, after Flask-Bootstrap includes its own.

  • #71 sanick sandel said 2019-10-28T03:41:56Z

    Hello Miguel,

    what can I do if I want integrate summernote text editor ? I tried but it doesn't work because wtf forms

  • #72 Miguel Grinberg said 2019-10-28T09:16:49Z

    @sanick: It may be possible to trick Flask-WTF into working with summernote. You would need to create a hidden TextArea field for Flask-WTF, and then from JavaScript when the form is submitted you can copy the text from summernote into the hidden text area.

  • #73 linlin said 2019-11-09T17:27:50Z

    I was trying to add an Image as background, image shows up locally but doesnt rendered on localhost. i tried all static folder, url_for, direct css, but nothing didnt work any suggestion please?

  • #74 Miguel Grinberg said 2019-11-09T18:05:40Z

    @linlin: you need to be more clear on what you mean by "locally" and by "on localhost". Aren't they the same thing? Also I suggest you debug this problem from the browser. If you open the debugger console you should be able to see if any requests are failing and why.

  • #75 Chuck said 2019-12-05T13:36:58Z

    You made massive changes to ALL of the template html files without explaining a single line of it. You explained how it happened, but then ask us to simply review the changes on github.

Leave a Comment