How do I add a custom options page to the panel?


I created a plugin which registers a custom widget. But I got stuck by adding some kind of settings page/modal to the widget.

// my-plugin/my-plugin.php
$kirby->set( 'widget', 'widget', __DIR__ . '/widget' );
$kirby->set( 'route', array(
    'pattern' => 'panel/gearsdigital/settings',
    'action'  => function () {
        return tpl::load( __DIR__ . DS . 'templates/settings.php');
) );

// my-plugin/widget/widget.php
return array(
    'title' => array(
        'text'       => 'Gearsdigital',
        'compressed' => true
    'options' => array(
            'text' => 'Settings',
            'icon' => 'pencil',
            'link' => 'panel/gearsdigital/settings'
    'html'  => function () {
        return tpl::load( __DIR__ . DS . 'template.php' );


It’s totally possible to do what you want but unfortunately it not documented anywhere. I’m using panel routes, modals, and forms, for an upcoming plugin update. The best way to learn how to use them is reading the Panel source code.

I’ll briefly try to explain the process:

Panel Routes

Use panel()->routes() to register routes:

<?php // site/plugins/plugin-name/routes.php

        'pattern' => 'my/plugin/route',
        'method'  => 'GET|POST',
        'filter'  => 'auth',
        'action'  => 'PluginName\Controller::config'

The route pattern will be automatically prefixed with panel/. You’ll find route examples at panel/app/config/routes.php.


Although one could use modal and forms straight inside a route closure, using a controller makes it easier in my opinion. The controller needs to extend Kirby\Panel\Controllers\Base and implement the view and form methods.

<?php // site/plugins/plugin-name/controller.php

namespace PluginName;

class Controller extends \Kirby\Panel\Controllers\Base
    public function config()
        $configuration = 'fetch configuration';

        $form = $this->form('config', compact('configuration'), function ($form) use ($configuration) {
            // this will be called only when the form POSTs to this action
            try {
                // save configuration…

            } catch (Exception $e) {
                panel()->alert('Something went wrong!');

            panel()->redirect(); // go back to dashboard

        return $this->modal('config', compact('configuration', 'form'));

    public function form($id, $data = array(), $submit = null)
        $file = kirby()->roots()->plugins() . DS . 'plugin-name' . DS . 'forms' . DS . $id . '.php';

        return panel()->form($file, $data, $submit);

    public function view($file, $data = array())
        return new View($file, $data);

You can add as many methods as needed. You’ll find controller examples at panel/app/controllers.

Take a look at panel/app/src/panel/controllers/base.php to understand what the base controller does under the hood.


The form method tells the controller where to look for forms, in this case it’s a form folder inside the plugins folder: site/plugins/plugin-name/forms.

You’ll find many form examples at panel/app/forms, just copy one of those to site/plugins/plugin-name/forms/config.php and tweak it to your needs.


You’ll need to create a class that extends Kirby\Panel\View to tell the controller where to look for views:

<?php // site/plugins/plugin-name/view.php

namespace PluginName;

class View extends \Kirby\Panel\View
    public function __construct($file, $data = [])
        $this->_root = kirby()->roots()->plugins() . DS . 'plugin-name' . DS . 'views';
        $this->_file = $file;
        $this->_data = $data;

In this case it’s a views folder inside the plugins folder: site/plugins/plugin-name/views. This is where you can put modal views:

<!-- site/plugins/plugin-name/views/config.php-->

<div class="modal-content modal-content-medium">
  <?php echo $form ?>

Note that we are passing the $form variable to this view on the controller config() method. You can pass more variables too.


Next up, let’s load these files:

<?php // site/plugins/plugin-name/plugin-name.php

  'pluginname\\controller' => __DIR__ . DS . 'controller.php',
  'pluginname\\view'  => __DIR__ . DS . 'view.php'

require 'routes.php';

I just copied these examples from my plugin (not releases yet) and changed them a bit. Let me know if something doesn’t work as expected.

Custom panel page (action function cannot be called statically), better way to do this?

WHOAAA! What an awesome answer :smile: l’ll try that out and come back to you later this day.


Just as a complement to the amazing answer by @pedroborges, you can find some undocumented documentation here:

There is a panel section there as well.


Also there is this boilerplate -

It has in it setup everything to add your own views and forms to the panel… It needs a bit of an update (to enable the text area to work properly) but except for that it functions.



Is there, in any way, a possibility to save all the data of this page to a custom .txt file in the content directory?


Yes, that should be possible. You might, however, just as well create a page, then, instead of a custom view? Maybe one you access directly via a widget, but hidden from the rest?


That 's indeed a better way of doing it, is there any example or documentation on how this could be done?


There are a lot of widget examples here. But all you really need to do is a simple widget with a link in it. Here are the docs:


Thanks… I tried it but I found no solution for the controller in my route based on tpl::load so I switched back to the old method ?key=value


@pedroborges I got this error:

Thanks for your help!


@tylerlumen Could you please post more context?

Well, the error message means exactly what is says, that you are using a $this variable outside of an object context (i.e. outside of a class instance), for example in a function…


Hey @texnixe , thanks for you answer!

I have use the code 1:1 from @pedroborges. See above: How do I add a custom options page to the panel?


Just that and nothing else?

And is there any more information of what file and line throws the error, maybe in your php error logs?


Now I can see the error:

But I don’t have a plan what I can do. :open_mouth:


I have only make a test with this code:

namespace newssystem;

class Controller extends \Kirby\Panel\Controllers\Base
    public function config()
        return $this->modal('config');

But the same error… Why I can’t use “$this” in my controller?..


Because the config function is called statically in the route action.