Skip to main content

How to Create Flask Forms with CKEditor and Flask-WTF?

Flask-CKEditor and Flask-WTF Forms

In this tutorial, we will be integrating Flask-WTF and Flask-CKEditor into your Flask application! You'll learn how to set up the editor, securely handle formatted HTML content, and create a seamless user experience that enhances any project that requires user-generated content. Let's get started!

Prerequisite:

This tutorial is part of the Flask CKEditor Project Series.

📚 View the Complete Flask CKEditor Series

                                                                                                                                                ➡ Next Part

Preliminary
Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies.
python -m venv venv
venv\Scripts\activate 
pip install flask flask-wtf flask-ckeditor
The next step is to set up an app structure; they are app.py, form.py, and the templates folder. Within the templates folder is contact.html. The app structure is described in the following diagram:

Contact form

Step 1: Create the basic minimum file configuration
Let us set up the app.py by importing the necessary module and configuration.
from flask import Flask, render_template

app = Flask(__name__)
app.config['SECRET_KEY'] = 'mysecretkey'

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

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8000, debug=True)
Meanwhile, the following is the minimum setup for contact.html. I have added a header and subheader at the top of the page.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contact Form</title>
</head>
      <body>  
       <h1>Keep In Touch 🤙</h1>
       <h5>We welcome your feedback and suggestions. We'll reply to you in 3 business days.</h5>
      </body>
</html>

Step 2: Set up the form widgets
Let us define what the widgets are in the form.py. For the name and email fields, I will use StringField and always validate them. For the subscription and button, I use BooleanField and SubmitField.
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField, SubmitField
from wtforms.validators import DataRequired

class ContactForm(FlaskForm):
      name = StringField('Name:', validators=[DataRequired()])
      email = StringField('Email:', validators=[DataRequired()])
      subscribe = BooleanField("Subscribe to our newsletter?")
      submit = SubmitField("Submit ☺️")
Then, we need to import the ContactForm from form.py into app.py and perform all the necessary setup in the index function.
from flask import Flask, render_template
from form import ContactForm

app = Flask(__name__)
app.config['SECRET_KEY'] = 'mysecretkey'

@app.route('/')
def index():
    # Create an instance of the ContactForm
    form = ContactForm()
    # The form object is sent to the template so it can be displayed and used
    return render_template('contact.html', form=form)

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8000, debug=True)
Now, we can set up a form to contain the widget in the contact.html.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Contact Form</title>
</head>
      <body>  
       <h1>Keep In Touch 🤙</h1>
       <h5>We welcome your feedback and suggestions. We'll reply to you in 3 business days.</h5>
       
       <form method="post" action="">
          {{ form.hidden_tag() }}
          <p>
              {{ form.name.label }}
              {{ form.name(size=32) }}<br>
          </p>
          <p>
              {{ form.email.label }}
              {{ form.email(size=32) }}<br>
          </p>
          <p>
              {{ form.subscribe() }} {{ form.subscribe.label }}
          </p>
          <p class="submit-container">
              {{ form.submit(class="btn-submit") }}
          </p>
      </form>
      </body>
</html>
Finally, let us check out how it looks.
Raw contact form

Step 3: Initialise and configure the Flask-CKEditor
First, let's import the module and initialise it in the app.py
from flask import Flask, render_template
from form import ContactForm
from flask_ckeditor import CKEditor

app = Flask(__name__)
# Initialize CKEditor and attach it to the Flask application
ckeditor = CKEditor(app)

app.config['SECRET_KEY'] = 'mysecretkey'
# Specify the CKEditor package type to load
app.config['CKEDITOR_PKG_TYPE'] = 'full-all'

@app.route('/')
def index():
    # Create an instance of the ContactForm
    form = ContactForm()
    # The form object is sent to the template so it can be displayed and used
    return render_template('contact.html', form=form)

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8000, debug=True)
Then, I have added the CKEditor widget in between the email and subscription widgets on the contact.html.
<p>
    {{ ckeditor.load() }}
    {{ ckeditor.config() }}
    <label>Comment:</label>
    {{ ckeditor.create() }}
</p>
The web page will display as the below view.
Unstyling contact form

Step 4: Style the contact.html 
To gain a better outlook, as shown in the cover diagram above, I have styled the contact.html file with CSS as follows:
<style>
    body {
        background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
        padding: 20px;
        font-family: 'Roboto', sans-serif;
        justify-content: center;
        align-items: center;
        margin: 50px 100px 50px 100px;
        border-radius: 5px;
    }

    h1 {
        color: #2c4d3d;
        text-align: center;
    }

    h5 {
        font-size: 18px;
        color: #333333;
        text-align: center;
        font-style: italic;
    }

    label {
        font-weight: bold;
        margin-top: 10px;
        padding-right: 50px;
    }

    input[type="text"],
    input[type="email"] {
        border: 4px solid #2c4d3d;
        border-radius: 5px;
        padding: 5px;
    }

    .btn-submit {
        justify-content: center;
        width: 200px;
        background-color: #2c4d3d;
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 5px;
        font-size: 16px;
        cursor: pointer;
    }

    .btn-submit:hover {
        background-color: #21b348;
        color: black;
    }

    p.submit-container {
        text-align: center;
    }

    .cke {
        border: 4px solid #2c4d3d !important;
        border-radius: 5px;
    }
</style>
To run the Flask app, in the terminal, type the command below:
python app.py
The following information will be shown, and just click the link provided: http://127.0.0.1:8000
URL address
Final wrap-up:
Currently, this implementation supports only CKEditor 4.22.1 (Full). Support for newer versions is not yet available, but future updates are expected as the ecosystem evolves. Stay tuned for improvements and enhancements ahead.

Published: Feb 2026
Last Updated: Feb 2026

---------------------------------------------------------------------------------------------------------------------------------------------------

🎁 Get Your FREE Flask Cheat Sheet

Join 500+ Python learners and receive this printable Flask Cheat Sheet instantly. Plus, get new Flask tutorials delivered to your inbox.

✓ Practical coding tutorials
✓ Automation tips for SMEs
✓ New project ideas and templates

-----------------------------------------------------------------------------------------------------------------------------
Need a similar system for your business? 
I build custom Flask web applications and Python automation solutions for SMEs and solopreneurs. 


-----------------------------------------------------------------------------------------------------------------------------
About the Author 

Kelvin Loh is a Python developer focused on Flask, desktop applications, and business automation solutions. He shares practical tutorials and real-world coding projects to help developers and small businesses build useful applications.

Comments

Popular Posts

How to Build an Audiobook Workflow Desktop System with Python?

In this tutorial, we will build a simple audiobook player using Python and CustomTkinter. You will learn how to convert text into speech using gTTS and play it with PyGame. We will also implement play, pause, and stop controls, like those in a real audio player. By the end, you will have a clean and functional desktop audiobook app. Prerequisite: This tutorial is part of the standalone tutorial. 📚 View the standalone tutorial Preliminary   Before I begin, it is recommended to activate the virtual environment before installing the relevant dependencies. python -m venv venv venv\Scripts\activate pip install customtkinter pillow gTTS pygame CTkMessagebox pypdf Then, the following steps include setting up the file structure, app.py, and two additional folders: the uploads and media folders. The media folder contains the icons necessary to build the app; there are read, pause, and stop icons, as shown on the diagram. Step 1: Build up the app interface I have 4 sections here: the...

How to Set Up PgAdmin and Adminer Using Docker Compose?

If you are new to database management with Docker, this tutorial will guide you through setting up both PgAdmin and Adminer using Docker containers. By containerising these tools, you can quickly launch lightweight and portable database management environments without installing them directly on your operating system. This approach also makes it easier to manage configurations, updates, and multiple projects across different devices.