Nested Shortcodes (container)

⌘K
  1. Home
  2. Docs
  3. Developers “How To&...
  4. Nested Shortcodes (container)

Nested Shortcodes (container)

This document explains how to create and work with nested elements (container elements that can hold other elements), such as tabs, accordions, and custom container components.

What are Nested Elements?

Nested elements are container elements that can hold child elements. Common examples include:

  • Tabs (vc_tta_tabsvc_tta_section)
  • Accordions (vc_tta_accordionvc_tta_section)
  • Call to Action boxes (container → buttons, text, etc.)

Simple examples in shortcode format:

[tabs]
    [tab title="Tab 1"]Content for Tab 1[/tab]
    [tab title="Tab 2"]Content for Tab 2[/tab]
[/tabs]

[accordion]
    [accordion_section title="Section 1"]Content for Section 1[/accordion_section]
    [accordion_section title="Section 2"]Content for Section 2[/accordion_section]
[/accordion]

[your_gallery param="foo"]
    [single_img]
    [single_img]
    [single_img]
[/your_gallery]

Types of Nested Elements

1. Container Elements (Parent)

Elements that can contain other elements (e.g., Tabs, Accordion, Custom Box)

2. Child Elements (Section)

Elements designed to be used inside containers (e.g., Tab Section, Accordion Section)

3. Multi-level Nesting

Elements that can both contain and be contained (e.g., Columns can contain elements and be inside rows)

Creating a Parent (Container) Element

Configuration Requirements

A container element requires special configuration properties:

<?php
/**
 * Configuration for parent container element
 */

return [
    'name' => __( 'Content Tabs', 'text-domain' ),
    'base' => 'content_tabs',
    'icon' => 'icon-wpb-tabs',
    'category' => __( 'Content', 'text-domain' ),
    'description' => __( 'Tabbed content container', 'text-domain' ),

    // Required for containers
    'is_container' => true,                    // Mark as container
    'show_settings_on_create' => false,        // Don't show settings popup immediately

    // Define allowed children
    'as_parent' => [
        'only' => 'content_tab'                // Only allow content_tab as child
        // or use 'except' => 'vc_row' to allow all except specific elements
    ],
    // Default content (initial child elements)
    'default_content' => '[content_tab title="Tab 1"][/content_tab][content_tab title="Tab 2"][/content_tab]',

    // JavaScript controller for admin editor
    'js_view' => 'VcColumnView',              // Need to specify for inner elements
    'params' => [
        // Container parameters
        [
            'type' => 'dropdown',
            'heading' => __( 'Tab Style', 'text-domain' ),
            'param_name' => 'style',
            'value' => [
                __( 'Default', 'text-domain' ) => 'default',
                __( 'Modern', 'text-domain' ) => 'modern',
            ],
        ],

        // ... more parameters
    ],
];

Key Container Properties

Property

Type

Description

is_container

bool

Must be true for containers

show_settings_on_create

bool

Show settings panel on creation

as_parent

array

Define allowed child elements

default_content

string

Initial child elements (shortcode format)

js_view

string

JavaScript view controller

content_element

bool

Set to false to hide from add elements panel

allowed_container_element

bool

Set to false to prevent being used as container

as_parent Configuration Options

// Option 1: Only specific elements allowed

'as_parent' => [
    'only' => 'content_tab'  // Single element
]

// Option 2: Multiple elements allowed

'as_parent' => [
    'only' => 'content_tab,another_element'  // Comma-separated
]

// Option 3: All except specific elements

'as_parent' => [
    'except' => 'vc_row,vc_column'  // Block these elements
]

// Option 4: No restrictions

'as_parent' => [
    'only' => ''  // Allow all elements
]

Container Template

<?php

/**
 * Template for [content_tabs] container
 */

if ( ! defined( 'ABSPATH' ) ) {
    die( '-1' );
}

/**
 * @var array $atts
 * @var string $content - Contains child shortcodes
 * @var WPBakeryShortCode_Content_Tabs $this
 */

$atts = vc_map_get_attributes( $this->getShortcode(), $atts );
extract( $atts );
$wrapper_classes = [ 'content-tabs', 'content-tabs--' . $style ];
$output = '';
$output .= '<div class="' . esc_attr( implode( ' ', $wrapper_classes ) ) . '">';
$output .= '<ul class="content-tabs__nav" role="tablist">';

// Parse child shortcodes to build tab navigation
$tabs = $this->getChildSections( $content );
foreach ( $tabs as $index => $tab ) {
    $active = $index === 0 ? ' class="active"' : '';
    $output .= '<li' . $active . '>';
    $output .= '<a href="#tab-' . $index . '" role="tab">';
    $output .= esc_html( $tab['title'] );
    $output .= '</a>';
    $output .= '</li>';
}

$output .= '</ul>';

// Render tab content (child shortcodes)

$output .= '<div class="content-tabs__content">';
$output .= do_shortcode( $content );  // Process child shortcodes
$output .= '</div>';
$output .= '</div>';

return $output;

Container Class

<?php
/**
 * Class WPBakeryShortCode_Content_Tabs
 */

if ( ! defined( 'ABSPATH' ) ) {
    die( '-1' );
}

class WPBakeryShortCode_Content_Tabs extends WPBakeryShortCodesContainer {
    /**
     * Parse child shortcodes and extract data
     */

    protected function getChildSections( $content ) {
        $tabs = [];
        // Parse shortcodes in content
        preg_match_all( '/\[content_tab([^\]]*)\](.*?)\[\/content_tab\]/s', $content, $matches );

        if ( ! empty( $matches[1] ) ) {
            foreach ( $matches[1] as $index => $atts_string ) {
                // Parse attributes
                $atts = shortcode_parse_atts( $atts_string );
                $tabs[] = [
                    'title' => isset( $atts['title'] ) ? $atts['title'] : 'Tab',
                    'content' => $matches[2][ $index ],
                ];
            }
        }

        return $tabs;
    }

    /**
     * Define default template for child elements
     */

    public function getChildDefaultContent() {
        return '[content_tab title="' . __( 'Tab 1', 'text-domain' ) . '"][/content_tab]';
    }

}

Creating a Child Element

Configuration Requirements

<?php

/**
 * Configuration for child element
 */

return [
    'name' => __( 'Tab Section', 'text-domain' ),
    'base' => 'content_tab',
    'icon' => 'icon-wpb-tab',
    'category' => __( 'Content', 'text-domain' ),
    'description' => __( 'Single tab content', 'text-domain' ),
    // Required for child elements

    'as_child' => [
        'only' => 'content_tabs'  // Can only be used inside content_tabs
    ],

    // Can also be a container itself

    'is_container' => true,
    'content_element' => true,  // Show in elements panel (usually false for children)
    'js_view' => 'VcColumnView',               // Need to specify for inner elements
    'params' => [
        [
            'type' => 'textfield',
            'heading' => __( 'Tab Title', 'text-domain' ),
            'param_name' => 'title',
            'admin_label' => true,  // Show in element title
            'value' => __( 'Tab', 'text-domain' ),
        ],
        [

            'type' => 'textarea_html',
            'heading' => __( 'Tab Content', 'text-domain' ),
            'param_name' => 'content',
            'value' => __( 'Tab content goes here', 'text-domain' ),
        ],
        [
            'type' => 'attach_image',
            'heading' => __( 'Tab Icon', 'text-domain' ),
            'param_name' => 'icon',
        ],
    ],
];

Key Child Properties

Property

Type

Description

as_child

array

Define allowed parent elements

content_element

bool

Show in add elements panel (usually false)

is_container

bool

true if child can contain other elements

as_child Configuration Options

// Only specific parent(s)
'as_child' => [
    'only' => 'content_tabs'  // Single parent
]

// Multiple parents
'as_child' => [
    'only' => 'content_tabs,another_container'  // Comma-separated
]

// All except specific parents
'as_child' => [
    'except' => 'vc_column'  // Block this parent
]

Child Template

<?php
/**
 * Template for [content_tab] child element
 */

if ( ! defined( 'ABSPATH' ) ) {
    die( '-1' );
}

/**
 * @var array $atts
 * @var string $content
 * @var WPBakeryShortCode_Content_Tab $this
 */

$atts = vc_map_get_attributes( $this->getShortcode(), $atts );
extract( $atts );

// Get index from parent
static $tab_index = 0;
$tab_id = 'tab-' . $tab_index;
$tab_index++;
$output = '';
$output .= '<div id="' . esc_attr( $tab_id ) . '" class="content-tab" role="tabpanel">';
// Add icon if provided
if ( ! empty( $icon ) ) {
    $icon_src = wp_get_attachment_image_src( $icon, 'thumbnail' );
    if ( $icon_src ) {
        $output .= '<img src="' . esc_url( $icon_src[0] ) . '" alt="" class="content-tab__icon" />';
    }
}
// Add content

$output .= '<div class="content-tab__content">';

$output .= wpb_js_remove_wpautop( $content, true );
$output .= '</div>';
$output .= '</div>';

return $output;

Child Class

<?php

/**
 * Class WPBakeryShortCode_Content_Tab
 */

if ( ! defined( 'ABSPATH' ) ) {
    die( '-1' );
}

class WPBakeryShortCode_Content_Tab extends WPBakeryShortCode {

    /**
     * Get parent element
     */

    protected function getParent() {
        return $this->parent;
    }

    /**
     * Get sibling elements
     */

    protected function getSiblings() {
        $parent = $this->getParent();
        if ( $parent ) {
            return $parent->get_children();
        }

        return [];
    }

   
    /**
     * Get element index among siblings
     */

    protected function getIndex() {
        $siblings = $this->getSiblings();
        return array_search( $this, $siblings, true );
    }
}

Working with Nested Content

Accessing Child Elements from Parent

// In parent class
public function getChildElements() {
    return $this->get_children();
}

// Parse child shortcodes manually
preg_match_all( '/\[child_element([^\]]*)\]/i', $content, $matches );
Accessing Parent from Child
// In child class
public function getParentElement() {
    return $this->parent;
}

// Get parent attributes
$parent_atts = $this->parent->getAttributes();


Passing Data Between Parent and Child
// In parent template

$GLOBALS['parent_style'] = $style;
$output .= do_shortcode( $content );
unset( $GLOBALS['parent_style'] );

// In child template

$parent_style = isset( $GLOBALS['parent_style'] ) ? $GLOBALS['parent_style'] : 'default';

 

Common Patterns

Tabs/Accordion Pattern

  • Parent generates navigation
  • Children generate content panels
  • JavaScript handles interaction

List/Item Pattern

  • Parent wraps items in container
  • Children render individual items
  • Parent can count/style items

 

Best Practices

  1. Use descriptive element names that indicate the relationship
  2. Set content_element = false for children that should only appear inside parents
  3. Provide default_content to help users get started
  4. Use admin_label on child titles to show content in editor
  5. Validate parent/child relationships in your classes
  6. Handle empty content gracefully
  7. Consider keyboard navigation for interactive nested elements
  8. Test deeply nested scenarios (3+ levels)

 

Troubleshooting

Children not appearing in editor

  • Check as_child configuration
  • Verify parent as_parent allows the child
  • Ensure content_element is true or omitted

Parent not acting as container

  • Confirm is_container is true
  • Check js_view is set correctly
  • Verify class extends WPBakeryShortCodesContainer

Content not rendering

  • Make sure to call do_shortcode( $content ) in parent template
  • Check for proper shortcode nesting syntax
  • Verify both elements are registered

 

 

Was this article helpful to you? No Yes

How can we help?