Before we begin, let’s clearly define the scope of this tutorial. We will:

  • Set up a “seconds” custom field in the backend
  • Format the “seconds” data to display a more readable time on the frontend
  • Insert the formatted time in via the_title filter

The final result will look in our Course Content block:

Formatted Time Duration in LearnDash Course Content

And this in Focus Mode (and the Navigation widget):

Formatted Time Duration in LearnDash Focus Mode

We will not be covering any sort of automation, that means:

  • We will not set up a one-size-fits-all “words per minute” calculation
  • We will not do any sort of per-user “words per minute” setting
  • We will not do any sort of per-user reading speed tracking
  • We will not query video provider APIs for duration data
  • We will not query locally hosted videos for duration data
  • We will not be summing the time of lesson substeps

I hope it goes without saying that any of the above “we will not” items would greatly complicate the scope of the tutorial.

We can always get the foundation in place and iterate upon it later.

Step #1: Enable “custom fields” support for Lessons and Topics

Contrary to popular belief, custom fields are a core WordPress feature.

While I am a fan of libraries like ACF and Carbon Fields, we’ll be using the WordPress-built-in custom field functionality.

To enable this core WordPress functionality, add the following PHP code snippet:

add_action( 'init', 'lmscoder_custom_fields_support_learndash' );
function lmscoder_custom_fields_support_learndash() {
	$post_types = [
	foreach ( $post_types as $post_type ) {
		add_post_type_support( $post_type, 'custom-fields' );

If you opt to use a custom fields library, be sure data is stored/returned in “seconds” (i.e. literally just a number like 127) so you can follow along.

Step #2: Confirm “custom fields” support for Lessons and Topics

After activating the above snippet, you should see a new “Custom Fields” checkbox option in Screen Options when you’re editing a Lesson.

Custom Fields option in Screen Options

Be sure to check it off if it is not already.

Step #3: Add duration_in_seconds key-value pairs

After enabling Custom Fields screen option, you’ll see a new panel in your Edit screen.

Go to all the Lessons and Topics you’d like to add duration_in_seconds to, and add a value.

A duration_in_seconds key-value pair.

For example, in this Lesson, I added a value of 500 to the duration_in_seconds key (sometimes referred to as a Name).

Step #4: Display the data

We’ll worry about formatting (making the time look readable) and styling (making the time look good) in the later steps.

Step #4.1: Add a helper function to get duration

This might seem a bit silly right now, although it will come in handy when it comes to formatting.

// Helper function to return duration
function lmscoder_get_duration( $post_id ) {
	return get_post_meta( $post_id, 'duration_in_seconds', true );

Instead of wrapping each instance of the post meta with a formatting function, we can do it in one place.

Step #4.2: Display the data by filtering the_title

There are a number of ways to display the data. There may be a hook available. You could always override a template.

However, when it came to this use case, I decided filtering the_title worked well enough and keeps this tutorial simpler.

To display the data for any post with the duration_in_seconds custom field key, add this onto your existing snippet:

// Display duration in lesson row
add_filter( 'the_title', 'lmscoder_add_time_to_title', 10, 2 );
function lmscoder_add_time_to_title( $title, $id ) {
	$formatted_time = lmscoder_get_duration( $id );
	if ( $formatted_time AND ! is_admin() ) {
		$title = $title . '<span class="course-step-duration">' . $formatted_time . '</span>';
	return $title;

Note that the time is not formatted yet, we’re just naming variables in our code as if it were because we will be formatting time in an upcoming step.

Step #4.3: Observe the effects

Filtering the_title is not an ideal option because it filters each and every output of a WordPress post title.

Unformatted duration data display on the frontend.

There is no distinction between titles in Focus Mode navigation, and the main heading of an individual Lesson post or breadcrumb item.

There isn’t even any distinction between frontend and backend, although that was addressed with the AND ! is_admin() check in the snippet.

If it wasn’t, our backend could look like so:

Filtering the_title can affect the backend

Note that HTML is escaped, which is why the HTML is seen like that.

Step #4.4: Style the time

Our goal is to only show duration in the Focus Mode sidebar (i.e. the Navigation widget), and the Course Content block.

Since titles can show up in a lot of places, we’ll default to hiding all .course-step-duration elements with the display: none !important; rule.

.course-step-duration {
	display: none !important;
	background: #db2020;
	color: #fff;
	font-size: 90%;
	padding: 5px;
	margin-left: 10px;
	font-weight: bold;
	display: inline-block;
	line-height: 1;
	position: relative;
	top: -.1em;

Also included are some visual styles, that you may want to tweak on your own to better fit your site.

Then use CSS to unhide it where we want it to be shown.

.ld-course-navigation .course-step-duration,
.ld-item-list .course-step-duration {
	display: inline-block !important;

If you’re using a theme that overrides LearnDash templates like BuddyBoss, you may need to adjust the selectors to account for that.

At this point, we start to see things take shape.

Styled time, unformatted. Breadcrumbs to be addressed later.

We will circle back to breadcrumbs later, because out-of-the-box, HTML (and therefore, our CSS selector) is stripped out.

Step #4.5: Format the time

We’re going to assume your lesson durations can be measured in hours, minutes, and seconds.

If your lesson durations last days, weeks, months, or years…consider shortening your lesson duration because that seems like a pretty long time for a single lesson to last.

I whipped up a small helper function that I hope is easy enough to read.

// Helper function to format time in seconds
function lmscoder_seconds_to_time( $seconds ) {
	$seconds = intval( $seconds );
	$hours = floor( $seconds / HOUR_IN_SECONDS );
	$minutes = floor( ( $seconds / 60 ) % MINUTE_IN_SECONDS );
	$seconds = floor( $seconds % MINUTE_IN_SECONDS );
	$time = '';

	if ( $hours >= 1 ) {
		$time .= $hours . 'hr, ';
	if ( $minutes >= 1 ) {
		$time .= $minutes . 'min, ';

	$time .= $seconds . 'sec';

	return $time;

There are many ways of formatting time. Basically, you pass this function a number of seconds, and a time formated like “1 hr, 5 min, 30 sec” is returned.

We’ll also alter our previous helper function to incorporate this one. So change your lmscoder_get_duration function to look like this:

// Helper function to return duration
function lmscoder_get_duration( $post_id ) {
	$seconds = get_post_meta( $post_id, 'duration_in_seconds', true );
	if ( ! $seconds ) {
		return false;

	$formatted_time = lmscoder_seconds_to_time( $seconds );

	return $formatted_time;

No changes are needed in our lmscoder_add_time_to_title function because it already referenced the lmscoder_get_duration function.

At this point, our Focus Mode sidebar should look like this:

Focus Mode sidebar with formatted duration.

With a similar effect in our Course Content block! Really started to take shape now, just one more step to go.

Step #5: Template override breadcrumbs

The specific template file we’re looking for can be found here: wp-content/plugins/sfwd-lms/themes/ld30/templates/modules/breadcrumbs.php

So open that, and copy the contents over to a new file here: wp-content/uploads/learndash/templates/ld30/modules/breadcrumbs.php

The issue is that the breadcrumbs are a bit overzealous with escaping and stripping tags.

You can resolve this by replacing 33:

<span><a href="<?php echo esc_url( $breadcrumbs[ $key ]['permalink'] ); ?>"><?php echo esc_html( wp_strip_all_tags( $breadcrumbs[ $key ]['title'] ) ); ?></a> </span>

With this:

<span><a href="<?php echo esc_url( $breadcrumbs[ $key ]['permalink'] ); ?>"><?php echo wp_kses_post( $breadcrumbs[ $key ]['title'] ); ?></a> </span>

Basically, replacing the esc_html and wp_strip_all_tags with wp_kses_post.

This function “sanitzes content for allowed HTML tags for post content” according to the WordPress developer documentation.

Step #6: Consider these thoughts

This should be considered a very rough draft and a sampling of what can be done with custom field data in LearnDash.

And when I say “in LearnDash” I really mean WordPress…because the_title is 100% a WordPress thing.

However, if this were for a paid client project, I would likely not use the_title filter and instead opt for template overrides.

Food for thought: If it’s not to much trouble, why not remove unwanted elements from HTML markup instead of hiding with CSS?

As outlined in the “we will not” list above, this concept can be taken a lot further, including automatically grabbing data from video providers like Vimeo.

This is not impossible to do, as Vimeo does have an API that lets you grab duration.

At that point, the app is no longer self-contained and concerns like hitting rate limits need to be considered.

Pro Tip: Do not constantly ping Vimeo for a duration when it is unlikely that would ever change. If it did, such data should be manually refreshed.

If you have any specific needs not covered by this tutorial, you can hire me and we can work together figuring out a solution for your use case.