Creating a To Do list WordPress theme

You might not know this about me but I used to be a WordPress developer, I used to get paid to exclusively develop Themes and Plugins at the company I worked for. It has been a while and I've lost the edge when it comes best practices and latest news. Specially around PHP and MySQL. It took me so long to be proficient at creating complex queries and now that's lost in some corner of my brain ๐Ÿ˜ญ I still try to follow it but only on personal interested, not professional.

If you know me, you know I love WordPress โค๏ธ It's truly a great platform for many use cases (many moons have pasted since WordPress was only used for blogs). As any stack, you need to consider the pros and cons. It doesn't work for all situations. I'll even say a good chunk of WordPress would be better off using Jamstack instead.

For my personal blog and sites (which I have probably too many) I've used both WordPress and static site generators. In the end, when it comes to UX having a CMS dashboard like the one you get out of the box with WordPress it's very convenient.

๐Ÿคš Enough old stories! Why are we here?

So, the other day I wanted to see what it would take to create a To Do list using WordPress as a backend system. I know there are a bunch of other tools that would be better suited for this. From simply using LocalStorage or other Client-Side storage (no backend at all), to Firebase or maybe the already mentioned Jamstack.

This is a Proof of Concept (PoC) so everything goes, overkill or not ๐Ÿ˜ผ


Using a Frontend Build Tools here is totally optional if you're not comfortable. I love a good FEBT, love to create them and, for speed in this PoC, it was just a copy and paste of the package.json from another project. Extra goodies and performance for free.

Here's what Github had to say about the language distribution in the repo:


  • I want to have a frontend UI to create new to-do items. I don't want to go anywhere near the WordPress Dashboard.
  • I want it to be fast, with JS, no page reloads.
  • I want to be able to mark items as done.

Using the WordPress Editor in the frontend

I've tried it. It's rather super simple to use:

<?php wp_editor( '', 'myId' ); ?>Code language: HTML, XML (xml)
  • Pros: 1 line creates a WordPress classic editor in the frontend ๐Ÿคฏ It will magically enqueue the stylesheets and scripts for you.
  • Cons: The darn thing weights about 1.5mb ๐Ÿ‘Ž

First I was excited then I quickly reverted the change and went back to using a good ol' <textarea> Which feels extra good to use plain and simple semantic HTML.

<label for="new-post">New item</label>
<textarea class="form-control todo__newPostContent" id="new-post" rows="3" required></textarea>Code language: HTML, XML (xml)

I could understand using this if you had the need of a rich text field. Using strong, italic, bullet lists, images... These were not requirements. The editor had to go.

FYI Code Reference: wp_editor()

WordPress REST API

A couple of releases back WordPress added a REST API. This allows developers to create headless WordPress sites. Fully JavaScript sites, if you want.

This was not my intention here. I am unfamiliar with the REST API. I did a quick glance and the authentication part seems a little more cumbersome that using the simpler admin-ajax feature. I was already conformable with this method which I used in my Thumbs Rating plugin a long time ago (shameless plug: it has more than 3.000 active installs. Blows my mind).

Should you go for the REST API?

Yes. It's more modern approach. The documentation says:

Even if youโ€™re using vanilla JavaScript or jQuery within a theme or plugin the REST API provides a more predictable and structured way to interact with your siteโ€™s content than admin-ajax, enabling you to spend less time accessing the data you need and more time creating better user experiences.

If you want a structured, extensible, and simple way to get data in and out of WordPress, you probably want to use the REST API.

P.S. Do as I say, not as I do ๐Ÿ™ƒ

Wait a minute Ricard, did you use jQuery?

No. No worries. I've used the Fetch API. Back in February I wrote a blog post on this exact topic: How to use WordPress admin-ajax with Fetch API.

Enqueue your script and define globals

Let's get started! First things first, let's include our JS file.

Go to your functions.php and enqueue the file you want to load in your page.

function todo_scripts() {
    get_template_directory_uri() . '/scripts.min.js', 

      'ajax_url' => admin_url( 'admin-ajax.php' ), 
      'nonce' => wp_create_nonce( 'todo-main-nonce' ) 
add_action( 'wp_enqueue_scripts', 'todo_scripts' );Code language: PHP (php)

This will load scripts.min.js at the footer of the page.

File in the repo: functions.php#L108

How to call WordPress using JavaScript?

Before we do the actual call and now that our JS file is being included in the page let's take a moment to define what will be our callback. The function that will be called when the AJAX Fetch request fires against the WordPress admin.

Here's how we define our AJAX callback, in our functions.php file:

if  ( ! function_exists( 'todo_create_post_callback' ) ):

	function todo_create_post_callback()

		// Check the nonce - security
		check_ajax_referer( 'todo-main-nonce', 'nonce' );

		// Do stuff

	add_action( 'wp_ajax_todo_create_post', 'todo_create_post_callback' );
	add_action('wp_ajax_nopriv_todo_create_post', 'todo_create_post_callback');

endif;Code language: PHP (php)

Now comes the juicy part. This is the main secret ingredient. What makes the whole PoC possible (since we already said we're not using the REST API).

Notice in the code below the global object todo_main_ajax which we have to defined in our functions.php when we enqueued the script file in the previous step.

Now use JS to call our defined callback, defined as action in the JavaScript data object:

const data = new FormData();

data.append('action', 'todo_create_post');
data.append('nonce', todo_main_ajax.nonce);
data.append('post_content', myTextareaElement.value);

fetch(todo_main_ajax.ajax_url, {
  method: 'POST',
  credentials: 'same-origin',
  body: data,
  .then((response) => response.json())
  .then((post) => {
    // Do stuff with what we decide to return from the PHP callback
  .catch((error) => {
    console.log('[To Do - Create Post]', error);
  });Code language: JavaScript (javascript)

Creating a post

At this point you have your JS successfully calling the WordPress Admin. The callback function we defined previously did nothing. Let me show you how can we create a post.

WordPress does it all for you. It has dozens of util functions to sanitize and manipulate content. Countless filters and arrays of settings to query data. In this case I'm using sanitize_textarea_field(). We can't trust the users, we need to:

  • Checks for invalid UTF-8
  • Converts single < characters to entities
  • Strips all tags
  • Preserves new lines (\n)
  • Strips octets

Here's how difficult it is to create a new post. This snippet is placed in the AJAX callback. Notice we're receiving the $_POST['post_content'] sent via the Fetch POST call.

$post = array(
  'post_content'  => sanitize_textarea_field($_POST['post_content']),
  'post_status'   => 'publish',
  'post_author'   => 1

$new_post_id = wp_insert_post( $post );Code language: PHP (php)

That's it. You're now creating posts dynamically every time you run that AJAX call.

Full code: create-post.php

Query posts

Using another callback to query posts is equally easy. You could use something like:

$args = array(
  'numberposts' => 10 // or -1 for all posts
$latest_posts = get_posts( $args );Code language: PHP (php)

But this is perhaps too easy for our use case. I wanted to query 2 sets of posts. The ones that are done, and the ones that aren't. Then I merge both arrays to present a unified list. The query is a little bit more complex but not too much. The important bit in this snippet is the meta_query that we use to filter the post by our meta field (keep reading to the next section):

$posts_done = query_posts(
        'numberposts' => -1,
        'suppress_filters' => false,
        'post_type' => 'post',
        'order'     => 'DESC',
        'orderby'   => 'date',
        'meta_query' => array(
            array('key' => 'todo__done',
                  'value' => true,
                  'type' => 'BINARY',
                  'compare' => '='
);Code language: PHP (php)

Now that we have our array of posts, let's render the results.

I've used JS template strings ๐Ÿ˜ to render the different items after receiving them via the Fetch promise:

 * @param  {object} data
 * @property  {string} data.title
 * @property  {string} data.post_date
 * @property  {string} data.post_content
 * @param  {HTMLElement} $loading
 * @returns {string}
  export function post(data, $loading) {
    let html = '';

    html += `
    <article class="todo__post position-relative row p-3 mb-3" data-${DATA_ATTRIBUTES.postID}="${data.ID}">
      <div class="${CLASSES.loading}">${$loading.innerHTML}</div>

  return html;
}Code language: JavaScript (javascript)

Of course I've separated the templates into different functions, as you can see we have a function for each part (the title, the content, the button...)

File in the repo: templates.js

Using post metadata

Another piece of magic right here ๐Ÿง™โ€โ™‚๏ธ

WordPress posts can have custom meta fields. You can store anything in there. We'll use a custom meta field to store a boolean. Is the post marked as done? (true/false)

How to update a WordPress post with custom meta field?

The same way we have created a callback function to create a new post, we need a new callback function that will be called with a different Fetch API POST. It's all very similar, the only different part is the PHP code that will be executed in the callback:

update_post_meta(intval($_POST['post_id']), 'todo__done', true);Code language: PHP (php)

It's so easy, feels like cheating. We're passing the post_id from the JS using Fetch POST and we're setting the todo__done as true

I'm also using the line above to set it to false when I'm creating the post. That way I can query the posts later by true or false.

Code Reference: update_post_meta()

Full code for the post-done.php callback: post-done.php

The need for speed

We're near the end now. The application now queries posts, can create new posts and we can mark those as "done". What's left? A little bit of performance.

Because performance matters, I've removed a bunch of JS and CSS that were loaded by default. WordPress does not expect us to hijack it the way I did.

Here's part of my functions.php to remove those extra Kb's.

function todo_remove_crap(){
	wp_dequeue_style( 'wp-block-library' );
	wp_deregister_script( 'wp-embed' );
	remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
	remove_action( 'wp_print_styles', 'print_emoji_styles' );
add_action( 'wp_enqueue_scripts', 'todo_remove_crap' );Code language: JavaScript (javascript)

Final touches

It couldn't be complete with some favicons, a web manifest, theme color... So you can install the app in your phone homescreen. I used my all time favorite tool to generate all the files for me using a GUI:

    "name": "To Do",
    "short_name": "To Do",
    "icons": [
            "src": "android-chrome-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
            "src": "android-chrome-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
    "theme_color": "#343a40",
    "background_color": "#343a40",
    "display": "standalone"
}Code language: JSON / JSON with Comments (json)

File in repo: site.webmanifest

More on the app manifest:


What would I do different?

This should be a plugin, not a theme. By creating a theme I've effectively removed functionality from WordPress. That's not good.

With this To Do Theme you can not write a blog, there's no way to write comments, have an archive, there's no styles for pages, media, blocks...

A plugin could add the functionality to an existing theme leveraging Custom Post Types. Thus, not killing other functionalities such as regular posts, pages, comments, etc...

Show me the code

I have a WordPress site running this Theme on a private instance, I can't show you that (sorry!). But you can take a look at the free full working code here. Fork it and enjoy!

Thank you for reading.


  1. Thanks a lot for great article!

Leave a Reply

Your email address will not be published. Required fields are marked *