What are Ctools plugins, and how does one create them?


2 Answers 2


Every now and then when working with Ctools Page manager, and Panels, it is useful to add custom Ctools plugins.

Ctools plugins comes in a large number of forms, and other modules, such as Feeds, Addressfield and Openlayers all make use of Ctools to provide plugins extensible by other modules. The most common forms of plugin though, are probably "content type", and "access". The first must not be confused with the entity "content" and its bundles, also called content types.

First, the boilerplate:

For any module to provide ctools plugins, they should first tell Ctools where to look for them. The hook below, says that we provide plugins for ctools, of the types "content_types", and "access". The function could be made simpler, but this way we ensure that only the right module gets told about the plugins, as well as only having it scan the disk for files when we actually provide the type of plugin being asked for.

function HOOK_ctools_plugin_directory($owner, $plugin_type) {
  // We'll be nice and limit scandir() calls.
  if ($owner == 'ctools' && ($plugin_type == 'content_types' || $plugin_type == 'access')) {
    return 'plugins/' . $plugin_type;

Below is an example directory structure for a module providing two plugins. One content type and one access plugin.


Content type plugin

A content type in the Ctools vocabulary, is more often known as a "Pane", as provided by for example Views. In this question: Is there a way to intercept a list of NID's created by a view and use them as a filter for another view?, the author asks about programmatically feeding arguments to a view. While that in it self it not very hard, the follow up question quickly becomes, "How do I display the results?".

One answer, will be to create a new "content type".

Now, the actual content type plugin, again using the Views question from above, could look like this:

$plugin = array(
  'title' => t('Render a View with arguments from another'),
  'single' => TRUE,
  'category' => array(t('My custom category'), -9),
  // Despite having no "settings" we need this function to pass back a form, or we'll loose the context and title settings.
  'edit form' => 'module_content_type_edit_form',
  'render callback' => 'module_content_type_render',

function module_content_type_render($subtype, $conf, $args, $context = NULL) {
  $block = new stdClass;
  $block->title = 'My View';

  $view = views_get_view('get_nids');
  $view->preview('display_machine_name', array($arg1, $arg2));

  $nids = '';
  foreach($view->result as $node) {
    $nids += $node->nid . ',';
  $nids = rtrim($nids, ',');
  $view = views_get_view('get_related');
  $view->execute_display('display_machine_name', array($nids));
  $block->content = $view->render();

  return $block;

 * 'Edit form' callback for the content type.
function module_content_type_edit_form($form, &$form_state) {
  // No settings beyond context, which has already been handled.
  return $form;

With this module enabled, there should now be a new category in Panels, 'My custom category', where in one should find a single pane, rendering the code from above.

Access plugin

The access plugin below will provide the ability to filer out variants and/or panes based on the depth of a term as measured from the root of the vocabulary.

 * @file
 * Plugin to provide access control based upon a parent term.

 * Plugins are described by creating a $plugin array which will be used
 * by the system that includes this file.
$plugin = array(
  'title' => t("Taxonomy: term depth"),
  'description' => t('Control access by the depth of a term.'),
  'callback' => 'term_depth_term_depth_ctools_access_check',
  'default' => array('vid' => array(), 'depth' => 0),
  'settings form' => 'term_depth_term_depth_ctools_access_settings',
  'settings form validation' => 'term_depth_term_depth_ctools_access_settings_validate',
  'settings form submit' => 'term_depth_term_depth_ctools_access_settings_submit',
  'summary' => 'term_depth_term_depth_ctools_access_summary',
  'required context' => new ctools_context_required(t('Term'), array('taxonomy_term', 'terms')),

 * Settings form for the 'term depth' access plugin.
function term_depth_term_depth_ctools_access_settings($form, &$form_state, $conf) {
  // If no configuration was saved before, set some defaults.
  if (empty($conf)) {
    $conf = array(
      'vid' => 0,
  if (!isset($conf['vid'])) {
    $conf['vid'] = 0;

  // Loop over each of the configured vocabularies.
  foreach (taxonomy_get_vocabularies() as $vid => $vocabulary) {
    $options[$vid] = $vocabulary->name;

  $form['settings']['vid'] = array(
    '#title' => t('Vocabulary'),
    '#type' => 'select',
    '#options' => $options,
    '#description' => t('Select the vocabulary for this form. If there exists a parent term in that vocabulary, this access check will succeed.'),
    '#id' => 'ctools-select-vid',
    '#default_value' => $conf['vid'],
    '#required' => TRUE,

  $form['settings']['depth'] = array(
    '#title' => t('Depth'),
    '#type' => 'textfield',
    '#description' => t('Set the required depth of the term. If the term exists at the right depth, this access check will succeed.'),
    '#default_value' => $conf['depth'],
    '#required' => TRUE,

  return $form;

 * Submit function for the access plugins settings.
 * We cast all settings to numbers to ensure they can be safely handled.
function term_depth_term_depth_ctools_access_settings_submit($form, $form_state) {
  foreach (array('depth', 'vid') as $key) {
    $form_state['conf'][$key] = (integer) $form_state['values']['settings'][$key];

 * Check for access.
function term_depth_term_depth_ctools_access_check($conf, $context) {
  // As far as I know there should always be a context at this point, but this
  // is safe.
  if (empty($context) || empty($context->data) || empty($context->data->vid) || empty($context->data->tid)) {
    return FALSE;

  // Get the $vid.
  if (!isset($conf['vid'])) {
    return FALSE;
  $depth = _term_depth($context->data->tid);

  return ($depth == $conf['depth']);

 * Provide a summary description based upon the checked terms.
function term_depth_term_depth_ctools_access_summary($conf, $context) {
  $vocab = taxonomy_vocabulary_load($conf['vid']);

  return t('"@term" has parent in vocabulary "@vocab" at @depth', array(
    '@term' => $context->identifier,
    '@vocab' => $vocab->name,
    '@depth' => $conf['depth'],

 * Find the depth of a term.
function _term_depth($tid) {
  static $depths = array();

  if (!isset($depths[$tid])) {
    $parent = db_select('taxonomy_term_hierarchy', 'th')
      ->fields('th', array('parent'))
      ->condition('tid', $tid)

    if ($parent == 0) {
      $depths[$tid] = 1;
    else {
      $depths[$tid] = 1 + _term_depth($parent);

  return $depths[$tid];
  • This is awesome! But is there any "official" documentation for this? (google finds a lot of blog posts but nothing "official" ..)
    – donquixote
    Commented Jun 20, 2013 at 22:43
  • 1
    There's plenty of examples in the ctools module itself, which is primarily where I picked things up. I've tried to pick up the task of writing official docs myself on more than one occasion, but always run out of steam.
    – Letharion
    Commented Jun 21, 2013 at 9:13
  • I have one problem when I follow this tutorial, and that is that the configuration form is not only blank, but lacks buttons.
    – beth
    Commented Jun 27, 2013 at 20:16
  • I somehow missed this question/answer first time round, awesome write up!
    – Clive
    Commented Jun 28, 2013 at 15:57
  • 2
    @beth The issue is $form in module_content_type_edit_form() should not be passed by reference.
    – Justin
    Commented Jul 16, 2013 at 18:55

CTools plugins are small files which can be part of any module as the way to extend its functionality. They can be used to provide components (panes), add additional styles options to your panels, etc.

Please check CTools Plugins without Panels page for step by step documentation. So briefly it goes like:

  1. You need to add CTools dependencies into your .info file as:

    dependencies[] = ctools
    dependencies[] = panels
  2. Tell CTools where your plugin is located:

    function MYMODULE_ctools_plugin_directory($module, $plugin) {
      if (($module == 'ctools') && ($plugin == 'content_types')) {
        return 'plugins/content_types';
  3. Implement plugin in a .inc file (by default as $module.$api.inc). Example plugin code:

    $plugin = array(
      'title' => t('Twitter feed'),
      'description' => t('Twitter feed'),
      'category' => 'Widgets',
      'icon' => '',
      'render callback' => 'twitter_block',
      'defaults' => array(),
    // render callback
    function twitter_block() {
      // Add twitter widget javascript
      $url = TWITTER_USER
      $widget_id = TWITTER_WIDGET_ID;
      $data = array();
      $data['url'] = $url;
      $data['widget_id'] = $widget_id;
      $content = array(
        '#theme' => 'my_block',
        '#content' => $data,
      $block = new stdClass();
      $block->content = $content;
      $block->title = '';
      $block->id = 'twitter_block';
      return $block;

The default location of the plugins looks like:


For more examples, please check ctools_plugin_example module which as part of CTools module, or checkout the Help page (CTools Plugin Examples) in Drupal UI after enabling the module.

In Drupal 8, this is now part of core (see: Drupal\Component\Plugin) and it provides Object inheritance, Object interfaces and Single file encapsulation. See: Drupal 8 Now: Object Oriented plugins in Drupal 7

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.