Four Kitchens

Creating custom panels panes (and use substitution too!)

6 Min. ReadDevelopment

I spent several hours last night just trying to add some configurable social sharing buttons to my node pane, but I needed to use fields from the node itself within my code. After hours of Google searching, and checking versions— I finally figured out how to do what I was looking for. Part of this confusion is due to just ctools (Chaos Tool Suite) having slightly different API depending on its version. Note, I am using ctools version 7.x-1.2 and panels version 7.x-3.3.

This quick tutorial will show you how to create you own custom panel, the form for settings, and finally how to use the substitution variables from the entity being rendered within your settings.

The .info File and .module File

First off, a custom panels pane is in reality a ctools content type, which has absolutely nothing to do with Drupal’s normal content types. This confusion of verbiage is found throughout a lot of documentation, so to differentiate here, I will only use the word “pane” to reference the ctools content type we are building. To create a pane, you must write your own custom module, although you can use an existing custom module if you want. Per usual, you will need an file, and will need to ensure that you have ctools set as a dependency.

name = My Custom Panes
description = Provides several custom panes to panels.
core = 7.x
dependencies[] = ctools

Next, you will need to alert ctools where to look for your custom pane information. This is done with hook_ctools_plugin_directory(), a hook that must be placed within your .module file. This hook will just return a directory path (from your module’s base folder) where ctools should go looking for your plugin files. See the example code below:

 * Implements hook_ctools_plugin_directory().
function mymod_ctools_plugin_directory($owner, $plugin_type) {
  if ($owner == 'ctools' && $plugin_type == 'content_types') {
    return 'plugins/' . $plugin_type;

In this example, the hook checks to see if this is for ctools ($owner), then if we are looking for the ‘content_types’ plugins. If you are going to be using more plugins, you can also include them into the ‘if’ statement, however we will not need any other ctools plugins for this example.

Define the Ctools Content Type (Pane)

The last file we will need is the file that will contain the full definition of the panel we want to make. It will live in the folder ‘plugins/content_types’ (taken from the hook return above) within your module directory. The file will have not only the definition, but also the settings form (if needed), and the render function. Do not worry about the name of the file, but ensure it is unique, and name spaced to your module. For this example, the file will be ”’ The first bit is the plugin definition, which will live outside a function, and the first piece of code within your file. Explanations of what each part means are inline.

$plugin = array(
  'single' => TRUE,  // Just do this one, it is needed.
  'title' => t('My Module Custom Pane'),  // Title to show up on the pane screen.
  'description' => t('ER MAH GERD, ERT DERS THINS'), // Description to show up on the pane screen.
  'category' => t('My Module'), // A category to put this under.
  'edit form' => 'mymod_pane_custom_pane_edit_form', // A function that will retu\r\n \the settings form for the pane.
  'render callback' => 'mymod_pane_custom_pane_render', // A function that will retu\r\n \the renderable content.
  'admin info' => 'mymod_pane_custom_pane_admin_info', // A function that will retu\r\n \the information displayed on the admin screen (optional).
  'defaults' => array( // Array of defaults for the settings form.
    'text' => '',
  'all contexts' => TRUE, // This is NEEDED to be able to use substitution strings in your pane.

At this point, once the module is enabled, then you pane should show up when you click to “Add content” to a panel. If it is not showing up, ensure that you do not have pending changes to the panel, as this will prevent updating of panes to occur.

Create a Settings Form

Next, we need to write the functions to create the edit form, render the pane, and create the admin info display. The form is created much like any other form with the FAPI. Your current settings are added to the $form_state variable, and the submit function will automatically saved any settings that are put into the $form_state[‘conf’] array.

 * An edit form for the pane's settings.
function mymod_pane_custom_pane_edit_form($form, &$form_state) {
  $conf = $form_state['conf'];

  $form['text'] = array(
    '#type' => 'textfield',
    '#title' => t('Panel Text'),
    '#description' => t('Text to display, it may use substitution strings'),
    '#default_value' => $conf['text'],

  return $form;

 * Submit function, note anything in the formstate[conf] automatically gets saved
 * Notice, the magic that automatically does that for you.
function mymod_pane_custom_pane_edit_form_submit(&$form, &$form_state) {
  foreach (array_keys($form_state['plugin']['defaults']) as $key) {
    if (isset($form_state['values'][$key])) {
      $form_state['conf'][$key] = $form_state['values'][$key];

Render the Pane

Now, we get to (finally!) actually render out content in our pane. For this, we will use the render function we defined above, and ensure that we return our content as a stdClass object with a ‘title’, and ‘content’. This can be either HTML, or renderable arrays, it does not matter. The importatant part here is the function ‘ctools_context_keyword_substitute’, which will take the $contexts variables passed through, and then replace the substitution strings within.

 * Run-time rendering of the body of the block (content type)
 * See ctools_plugin_examples for more advanced info
function mymod_pane_custom_pane_render($subtype, $conf, $args, $contexts) {

  // Update the strings to allow contexts.
  if (!empty($contexts)) {
    $content = ctools_context_keyword_substitute($conf['text'], array(), $contexts);

  $block = new stdClass();

  // initial content is blank
  $block->title = t('This is my title!'); // This will be overridden by the user within the panel options.
  $block->content = $content;

  return $block;

That’s it! You have made a rendered panel pane. You can of course make this more complex as you needs are. For me, I wrote a custom function that will take the $conf variables, and then create a “tweet this” button. It then had variables for the hashtags, url, and text that should be automatically added to the tweet, but the possibilities are endless.

The Admin Display Page

This last section is optional, but allows you to display information on the panel’s content configuration screen. By default, each pane will just display “No info available.” We have already defined our admin display within the $plugin definition above, so we just need to use the same function name we defined above:

 * 'admin info' callback for panel pane.
function mymod_pane_custom_pane_admin_info($subtype, $conf, $contexts) {
  if (!empty($conf)) {
    $block = new stdClass;
    $block->title = $conf['override_title'] ? $conf['override_title_text'] : '';
    $block->content = $conf['text'];
    return $block;

You can, of course, change the $block->content to something more complex, but that is all that is needed for this display.

Other Links

Here are some other tutorials I found very helpful in my search. They provide more examples of some of the functionality here.

I also included the code files used here for you to look at all together. They work, I swear.

And just remember… when in doubt. Clear the cache.