Transform Learning with a Powerful Enterprise LMS

Learn more
Moodle

Moodle Renderers and Output Classes: How a Renderer Plugin Works

Renderables, Mustache templates and renderer classes in Moodle 5.3: how render() picks a path, how themes override plugin output, and what 4.5 to 5.3 changed.

Learn more about this topic with AI

Gemini opens with the prompt copied - paste it into the chat to start.

Illustration of a Moodle renderer passing a renderable through a template to the theme, for the article on Moodle renderer plugins

A Moodle renderer plugin setup has three parts: an output class (a "renderable") that holds the data, a Mustache template or React component that turns it into markup, and a renderer class in classes/output/renderer.php that a theme can subclass to change the result. Your page script gets the renderer with $PAGE->get_renderer('local_demo') and calls render(). Everything else, including how themes override your output, follows from the class names you choose.

This explainer covers how render() picks a rendering path, how the renderer factory finds a theme override, the template override path, and what changed in recent branches: namespaced render methods in 4.5, dependency injection in 5.0, externable in 5.1 and React renderables in 5.3.

Checked against the Moodle 5.3.0 source (version 2026100500), its UPGRADING.md and the Output API page of the Moodle developer documentation, as of October 2026.

The three pieces: renderable, template, renderer

PieceWhere it livesJob
Renderableclasses/output/status_card.php, implementing \core\output\renderableHolds the data. All database queries, parameter handling and capability checks happen before it is built.
Templatetemplates/status_card.mustacheTurns the exported context into HTML.
Rendererclasses/output/renderer.php, extending \core\output\plugin_renderer_baseThe override point. It can stay empty.

The Output API docs put the rule plainly: the renderable "should hold all the data required" so that rendering only turns data into markup. Keeping logic out of templates and renderers is what makes theme overrides safe, because a themer can change markup without touching access checks.

In 4.5 the core output classes moved into namespaces (\renderable became \core\output\renderable, \plugin_renderer_base became \core\output\plugin_renderer_base, and so on, MDL-82183). The old names remain as class aliases, but new code should use the namespaced ones.

A minimal renderer plugin, end to end

The example is a local plugin, local_demo, that shows a status card. In 5.1 and later the code sits under public/local/demo/. Not run on a live site; test on staging first.

<?php
// public/local/demo/classes/output/status_card.php
namespace local_demo\output;

use core\output\named_templatable;
use core\output\renderable;
use core\output\renderer_base;

class status_card implements named_templatable, renderable {
    public function __construct(
        protected string $title,
        protected int $openitems,
    ) {
    }

    #[\Override]
    public function get_template_name(renderer_base $renderer): string {
        return 'local_demo/status_card';
    }

    #[\Override]
    public function export_for_template(renderer_base $output): \stdClass {
        return (object) [
            'title' => format_string($this->title),
            'openitems' => $this->openitems,
            'hasitems' => $this->openitems > 0,
        ];
    }
}
{{!
    @template local_demo/status_card

    Example context (json):
    { "title": "Onboarding", "openitems": 3, "hasitems": true }
}}
<section data-region="local_demo-status-card">
    <h3>{{title}}</h3>
    {{#hasitems}}<p>{{#str}} openitems, local_demo, {{openitems}} {{/str}}</p>{{/hasitems}}
    {{^hasitems}}<p>{{#str}} noitems, local_demo {{/str}}</p>{{/hasitems}}
</section>
<?php
// public/local/demo/classes/output/renderer.php
namespace local_demo\output;

class renderer extends \core\output\plugin_renderer_base {
}
<?php
// public/local/demo/index.php
require(__DIR__ . '/../../config.php');

require_login();
$context = context_system::instance();
require_capability('local/demo:view', $context);

$PAGE->set_context($context);
$PAGE->set_url(new moodle_url('/local/demo/index.php'));
$PAGE->set_title(get_string('pluginname', 'local_demo'));
$PAGE->set_heading(get_string('pluginname', 'local_demo'));

$output = $PAGE->get_renderer('local_demo');
echo $output->header();
echo $output->render(new \local_demo\output\status_card('Onboarding', 3));
echo $output->footer();

The capability local/demo:view and the strings openitems and noitems must be defined in your plugin's db/access.php and language file. Mustache escapes {{title}} on output; use triple braces only for HTML you have already cleaned with format_text().

How render() chooses a path

\core\output\renderer_base::render() tries these in order:

  1. A render_<classname>() method on the renderer. For renderables inside an output namespace, it first tries a name built from the sub-namespace with double underscores, so \core\output\action_menu\link maps to render_action_menu__link (added in 4.5, MDL-83164).
  2. A class implementing \core\output\react_component_renderable (5.3).
  3. A class implementing \core\output\named_templatable.
  4. A class implementing \core\output\templatable, with the template name guessed as component/classname.

Plugin renderers behave slightly differently. plugin_renderer_base::render() checks for a render_<classname>() method, then named_templatable, then a legacy render_<name>_renderable() method with a debugging notice, and finally hands the renderable to the core renderer, which handles the React and plain templatable cases. A _renderable suffix on the class name is stripped before the method name is built.

The docs recommend named_templatable for Mustache and react_component_renderable for new React code. Plain templatable still works but hides the template name. A render method takes priority over both, which is exactly why a theme can intercept your output.

How Moodle finds the renderer class

$PAGE->get_renderer($component, $subtype, $target) passes the request to the theme's renderer factory. Boost sets $THEME->rendererfactory = 'theme_overridden_renderer_factory', so child themes of Boost get override support. The factory builds candidate class names and tries them in this order:

  1. Theme-prefixed names for the current theme, then each parent theme, with the target suffix. For local_demo that is theme_mytheme\output\local_demo_renderer, then the legacy theme_mytheme_local_demo_renderer.
  2. The plugin's own class, \local_demo\output\renderer, then the legacy local_demo_renderer from renderer.php, with the suffix.
  3. The same two lists again without the suffix.

With a subtype, the plugin class becomes \local_demo\output\<subtype>_renderer or \local_demo\output\<subtype>\renderer. If nothing matches, you get a coding_exception "Request for an unknown renderer".

Targets

The target selects a suffix. Constants in lib/outputlib.php are RENDERER_TARGET_GENERAL, RENDERER_TARGET_CLI (_cli), RENDERER_TARGET_AJAX (_ajax), RENDERER_TARGET_TEXTEMAIL, RENDERER_TARGET_HTMLEMAIL and RENDERER_TARGET_MAINTENANCE. With no target, CLI scripts get _cli and AJAX scripts _ajax. That is how core ships core_renderer_cli and core_renderer_ajax. For email, request the target explicitly and add a renderer_textemail class if plain-text output needs different markup.

Overriding plugin output from a theme

A theme has two override levels, and the cheaper one usually wins.

Template override

The template finder searches theme/<theme>/templates/<component>/, then each parent theme, then the component's own templates/ folder. Copy status_card.mustache to theme/mytheme/templates/local_demo/status_card.mustache, change the markup and purge caches. Boost itself overrides core templates this way under theme/boost/templates/core/. The context stays the same, so only change markup that uses the variables the renderable exports.

Renderer override

When the data has to change, subclass the renderer:

<?php
// public/theme/mytheme/classes/output/local_demo_renderer.php
namespace theme_mytheme\output;

use local_demo\output\status_card;

class local_demo_renderer extends \local_demo\output\renderer {
    public function render_status_card(status_card $card): string {
        $data = $card->export_for_template($this);
        $data->compact = true;
        return $this->render_from_template('theme_mytheme/status_card', $data);
    }
}

Not run on a live site; test on staging first. Boost's own theme_boost\output\core_renderer follows the same pattern for core output. Keep overrides small: every overridden method is a contract with a plugin you do not control, and a plugin upgrade that changes the exported context breaks it quietly.

React renderables in Moodle 5.3

Moodle 5.3 adds \core\output\react_component_renderable (MDL-89296). It requires two methods: get_react_component_name(), which returns a path inside the @moodle/lms package, and get_react_component_props(), which returns a stdClass of initial props. The renderer prints a mount point through html_writer::react_component(): a div with data-react-component and JSON-encoded data-react-props attributes.

The developer docs ask you to keep props to initial configuration and preferences. Mustache habits such as passing translated strings or prefetched data into the context do not carry over. If a theme needs different behaviour, it can still add a render_<classname>() method, because that check runs before the React path.

Renderers in services, tasks and web services

  • Dependency injection (5.0, MDL-83888): classes built for \core\di can take \core\output\renderer_helper in the constructor and call get_renderer('local_demo') or get_core_renderer() instead of reading the $PAGE global.
  • Web services (5.1, MDL-85509): template context is not a stable API. To reuse an output class in an external function, implement \core\output\externable with an exporter class. The docs require a PHPUnit test that checks the exported structure and values.
  • Maintenance layout: on pages with the maintenance layout, get_renderer() forces the maintenance target, which blocks APIs that may be unavailable.

Common mistakes we see in plugin reviews

  • Logic in the renderer. Queries or has_capability() calls inside render methods. A theme override then skips or duplicates them.
  • Echoing HTML from the page script. Markup built with html_writer in index.php has no override point at all.
  • Using $OUTPUT for plugin output. The docs say plugins should use their own renderer, so themes can target them.
  • Unescaped context. Passing raw user text into triple-brace Mustache variables. Use format_string() or format_text() with the right context in export_for_template().
  • Override names that never match. A theme class in the wrong namespace fails silently and the plugin renderer is used instead. Check the class name against the factory order above.

Our Moodle plugin types guide explains which plugin type should own the output, the local plugin tutorial covers the files around this example, and the Hooks API example shows how to inject output into core pages without overriding a renderer. For the wider picture, start at our Moodle guide.

How we can help

We build and review Moodle plugins and themes, including renderer and template overrides that survive upgrades. See our LMS plugin development service or our Moodle development work.

Frequently asked questions

Does every Moodle plugin need a renderer class?

Any plugin that outputs HTML should have one, even if it is empty, because it is the class a theme subclasses to override output. With named_templatable, templatable or React renderables you do not need render methods in it.

What is the difference between templatable and named_templatable?

Both require export_for_template(). named_templatable also requires get_template_name(), so the template is explicit. With templatable, Moodle guesses the template as component/classname. The developer docs recommend named_templatable.

How does a theme override a plugin renderer?

Create theme_yourtheme\output\pluginname_renderer extending the plugin's renderer and add a render_<classname>() method. The theme must use theme_overridden_renderer_factory, which Boost and its child themes do.

Can I use React instead of Mustache in a Moodle plugin?

From Moodle 5.3, a renderable can implement core\output\react_component_renderable. It returns a component name in the @moodle/lms package and a small set of initial props, and Moodle prints the mount point.

Free 30-minute consultation

Not sure where to start? Talk to an LMS engineer.

Tell us what platform you run and what you need. You’ll get honest, practical advice - even if the answer is that you don’t need us.

Sources & references

  1. Output API (Moodle developer documentation) - Moodle HQ
  2. renderer_base::render() (Moodle 5.3.0) - Moodle HQ
  3. theme_overridden_renderer_factory (Moodle 5.3.0) - Moodle HQ
  4. UPGRADING.md (Moodle 5.3.0) - Moodle HQ
  5. Templates guide - Moodle HQ
Add as a preferred source on Google

Add All-in-One LMS as a preferred source to see more of our articles in Google’s Top Stories.

Learn more about this topic with AI

Gemini opens with the prompt copied - paste it into the chat to start.

Keep reading

Before you go - get a free LMS audit

Tell us which LMS you use and what is not working. An engineer replies within one business day.

Choose which optional cookies we may use. You can change this at any time from "Cookie preferences" in the footer. Cookie policy · Privacy policy