Creating Custom Tabs
Instructor content tabs use a Template Method pattern introduced in version 7.5.0. The LD_Dashboard_Tab_Base abstract class defines the rendering algorithm; subclasses implement the content-specific steps.
LD_Dashboard_Tab_Base
Section titled “LD_Dashboard_Tab_Base”Located at includes/tabs/class-ld-dashboard-tab-base.php.
All custom tabs must extend this class. The render() method is declared final — you cannot override it. Instead, implement the abstract methods that render() calls internally.
Required Properties
Section titled “Required Properties”protected $post_type; // WordPress post type for this tab (e.g., 'sfwd-lessons')protected $tab_name; // Tab identifier stringAbstract Methods (required)
Section titled “Abstract Methods (required)”| Method | Return | Description |
|---|---|---|
get_id(): string |
string |
Unique tab ID (e.g., gradebook). |
get_learndash_label_key(): string |
string |
LearnDash custom label key (e.g., lesson, quiz). Used by LearnDash_Custom_Label::get_label(). |
get_accessible_ids( array $params ): array |
array |
Return an array of post IDs the current user can see. Return empty array for admins (no restriction). |
get_filter_args( array $args, array $params ): array |
array |
Modify the WP_Query args array with tab-specific filters (status, course, etc.). |
render_item( WP_Post $post, array $params ): string |
string |
Render a single list item’s HTML. Must return a string. |
render() — Template Method
Section titled “render() — Template Method”The final public function render( array $params ): array method executes the tab rendering algorithm:
render( $params ) ├── get_accessible_ids( $params ) → restrict posts to user's scope ├── build_query_args( $params, $ids ) → WP_Query base args ├── get_filter_args( $args, $params ) → tab-specific filters ├── apply_filters('ld_dashboard_{id}_content_args', $args) → developer hook ├── new WP_Query( $args ) → execute query ├── render_items( $query, $params ) → calls render_item() per post ├── get_stats( $ids ) → count totals by status └── build_response( $content, $query, $page, $stats ) → apply_filters('ld_dashboard_{id}_content', $content) → apply_filters('ld_dashboard_{id}_content_data', $response)The response array contains:
array( 'content' => string, // Rendered HTML 'next' => bool, // Has next page 'prev' => bool, // Has previous page 'first' => bool, // Is not first page 'last' => bool, // Is not last page 'maxpages' => int, 'currentpage' => int, 'totalitems' => int, 'stats' => array( // Counts by post status 'total' => int, 'published' => int, 'draft' => int, 'pending' => int, ),)Access Control Helpers
Section titled “Access Control Helpers”The base class provides access control methods you can call inside render_item():
// Get full access status for a post$access = $this->get_access_status( $post );// Returns:// array(// 'can_edit' => bool,// 'can_delete' => bool,// 'badge' => 'your-content' | 'shared' | 'view-only',// 'badge_text' => string,// )
// Individual checks$can_edit = $this->user_can_edit( $post );$can_delete = $this->user_can_delete( $post );Badge values:
your-content(green) — User is the post author.shared(blue) — User can edit via course co-instructor relationship.view-only(gray) — User can see but not edit.
Edit permission logic:
Section titled “Edit permission logic:”- Administrator → always true.
- Group leader with admin capability → always true.
- Post author → always true.
- Co-instructor on the associated course → true.
Built-in Helper Methods
Section titled “Built-in Helper Methods”| Method | Description |
|---|---|
build_action_links( $post, $nonce, $access ) |
Returns HTML for view/edit/delete action links. |
get_elementor_edit_link( $post ) |
Returns an Elementor edit link if the post type supports it. |
render_empty_state( $search_term ) |
Returns empty-state HTML with a “Clear Filters” button. |
get_status_label( $status ) |
Returns a translated status label for publish, draft, pending. |
get_associated_course_id( $post_id ) |
Retrieves the course ID associated with a post via meta or learndash_get_course_id(). |
get_nonce_action() |
Returns {id}-nonce as the nonce action. |
get_edit_action() |
Returns edit-{id}. |
get_delete_action() |
Returns delete-{id}. |
get_tab_param() |
Returns my-{id}s. |
get_post_param() |
Returns ld-{id}. |
LD_Dashboard_Tab_Registry
Section titled “LD_Dashboard_Tab_Registry”Located at includes/tabs/class-ld-dashboard-tab-registry.php.
The singleton registry manages tab instances and dispatches AJAX requests.
Registering Custom Tabs
Section titled “Registering Custom Tabs”// Via filter (preferred — runs before any tab is instantiated)add_filter( 'ld_dashboard_registered_tabs', function( $tabs ) { $tabs['gradebook'] = 'My_Gradebook_Tab'; return $tabs;} );
// Via registry method (must run before AJAX dispatch)add_action( 'init', function() { LD_Dashboard_Tab_Registry::instance()->register( 'gradebook', 'My_Gradebook_Tab' );} );AJAX Dispatch
Section titled “AJAX Dispatch”The registry handles the ld_dashboard_get_instructor_tab_content and ld_dashboard_tab_content_filter AJAX actions. Your tab is automatically included in AJAX dispatch when registered.
// The registry dispatches to your tab via:$response = LD_Dashboard_Tab_Registry::instance()->render( 'gradebook', $params );AJAX params passed to render():
| Param | Type | Description |
|---|---|---|
course_id |
int | Filter by course |
lesson_id |
int | Filter by lesson |
topic_id |
int | Filter by topic |
quiz_id |
int | Filter by quiz |
page |
int | Pagination page |
search |
string | Search term |
grade_type |
string | Grade type filter |
Complete Example: Gradebook Tab
Section titled “Complete Example: Gradebook Tab”<?php/** * Gradebook Tab for LD Dashboard. * * @since 1.0.0 */class My_Gradebook_Tab extends LD_Dashboard_Tab_Base {
/** * Constructor. * * @since 1.0.0 */ public function __construct() { parent::__construct(); $this->post_type = 'sfwd-quiz'; $this->tab_name = 'gradebook'; }
/** * Get the unique tab ID. * * @since 1.0.0 * @return string */ public function get_id(): string { return 'gradebook'; }
/** * Get the LearnDash custom label key. * * @since 1.0.0 * @return string */ protected function get_learndash_label_key(): string { return 'quiz'; }
/** * Get post IDs accessible to the current user. * * @since 1.0.0 * @param array $params Request parameters. * @return array Post IDs the user can access. */ protected function get_accessible_ids( array $params ): array { $user_id = $this->current_user_id;
// Admins see all quizzes — return empty to skip post__in. if ( learndash_is_admin_user( $user_id ) || LD_Dashboard_Helper::group_leader_has_admin_cap() ) { return array(); }
// Instructors see quizzes attached to their courses. $course_ids = LD_Dashboard_Course_Helper::get_instructor_courses( $user_id );
if ( empty( $course_ids ) ) { return array( 0 ); // Force empty result. }
$quiz_ids = array(); foreach ( $course_ids as $course_id ) { $quizzes = learndash_get_course_quiz_list( $course_id ); foreach ( $quizzes as $quiz ) { $quiz_ids[] = $quiz['post']->ID; } }
return array_unique( $quiz_ids ) ?: array( 0 ); }
/** * Apply tab-specific filter args. * * @since 1.0.0 * @param array $args Base WP_Query args. * @param array $params Request parameters. * @return array Modified args. */ protected function get_filter_args( array $args, array $params ): array { // Filter by course. if ( ! empty( $params['course_id'] ) ) { $course_quizzes = learndash_get_course_quiz_list( absint( $params['course_id'] ) ); $course_quiz_ids = wp_list_pluck( array_column( $course_quizzes, 'post' ), 'ID' );
// Intersect with already-restricted IDs. if ( ! empty( $args['post__in'] ) && ! empty( $course_quiz_ids ) ) { $args['post__in'] = array_intersect( $args['post__in'], $course_quiz_ids ); } elseif ( ! empty( $course_quiz_ids ) ) { $args['post__in'] = $course_quiz_ids; } }
// Filter by status. if ( ! empty( $params['grade_type'] ) && 'all' !== $params['grade_type'] ) { $args['post_status'] = sanitize_key( $params['grade_type'] ); }
return $args; }
/** * Render a single quiz item. * * @since 1.0.0 * @param WP_Post $post Quiz post object. * @param array $params Request parameters. * @return string HTML content. */ protected function render_item( WP_Post $post, array $params ): string { $access = $this->get_access_status( $post ); $nonce = wp_create_nonce( $this->get_nonce_action() ); $attempts = learndash_get_user_quiz_attempts_by_quiz( $this->current_user_id, $post->ID );
ob_start(); ?> <div class="ld-dashboard-list-item ld-gradebook-item" data-post-id="<?php echo esc_attr( $post->ID ); ?>">
<div class="ld-dashboard-item-badge ld-badge--<?php echo esc_attr( $access['badge'] ); ?>"> <?php echo esc_html( $access['badge_text'] ); ?> </div>
<h4 class="ld-dashboard-item-title"> <?php echo esc_html( $post->post_title ); ?> </h4>
<div class="ld-dashboard-item-meta"> <span class="ld-gradebook-attempts"> <?php printf( /* translators: %d: number of attempts */ esc_html( _n( '%d attempt', '%d attempts', count( $attempts ), 'my-plugin' ) ), count( $attempts ) ); ?> </span> <span class="ld-dashboard-item-status ld-status--<?php echo esc_attr( $post->post_status ); ?>"> <?php echo esc_html( $this->get_status_label( $post->post_status ) ); ?> </span> </div>
<div class="ld-dashboard-item-actions"> <?php echo wp_kses_post( $this->build_action_links( $post, $nonce, $access ) ); ?> </div> </div> <?php return ob_get_clean(); }}
// Register the tab.add_filter( 'ld_dashboard_registered_tabs', function( $tabs ) { $tabs['gradebook'] = 'My_Gradebook_Tab'; return $tabs;} );Using the Tab via Registry
Section titled “Using the Tab via Registry”// Render programmatically.$response = LD_Dashboard_Tab_Registry::render_tab( 'gradebook', array( 'course_id' => 123, 'page' => 1, 'search' => '',) );
// $response['content'] contains the HTML.// $response['totalitems'] contains the total count.Extending Built-in Tabs
Section titled “Extending Built-in Tabs”You can replace a built-in tab by re-registering it with your own class:
add_filter( 'ld_dashboard_registered_tabs', function( $tabs ) { // Replace the quiz tab with a custom implementation. $tabs['quiz'] = 'My_Enhanced_Quiz_Tab'; return $tabs;} );My_Enhanced_Quiz_Tab can extend the original LD_Dashboard_Tab_Quiz to inherit its logic:
class My_Enhanced_Quiz_Tab extends LD_Dashboard_Tab_Quiz {
protected function render_item( WP_Post $post, array $params ): string { // Add custom fields before the default render. $extra_html = '<div class="my-extra-data">...</div>'; return $extra_html . parent::render_item( $post, $params ); }}Built-in Tab IDs
Section titled “Built-in Tab IDs”| ID | Class | Post Type |
|---|---|---|
lesson |
LD_Dashboard_Tab_Lesson |
sfwd-lessons |
topic |
LD_Dashboard_Tab_Topic |
sfwd-topic |
quiz |
LD_Dashboard_Tab_Quiz |
sfwd-quiz |
question |
LD_Dashboard_Tab_Question |
sfwd-question |
assignment |
LD_Dashboard_Tab_Assignment |
sfwd-assignment |
announcements |
LD_Dashboard_Tab_Announcement |
sfwd-announcements |
certificate |
LD_Dashboard_Tab_Certificate |
sfwd-certificates |

