How to create a tabbed options page for your WordPress theme using CMB
If you are a WordPress developer like me creating your own themes, you most probably would have wanted to add easy customisability to your theme by incorporating a theme options page. But you might have found out that it’s a big hassle to create one, having to write lots of code to render the options page and more to process the input data. It’s especially complicated if you want to do anything fancy like including file upload fields or color-pickers. Fortunately there are several WordPress options frameworks out there that make the job quite easy.
Personally I have been using the WP Theming Options Framework Theme for my recent themes’ options pages and Custom Metaboxes and Fields (CMB) for WordPress to create my custom metaboxes for those themes. However it was a bit of a hassle and redundant to use two separate frameworks for two very similar tasks. However it turns out that CMB is actually capable of creating a Theme Options page!
The CMB Wiki has a great example on how to create your own theme options page. But I wanted to go one step further and also add tabs to the options page. I have adapted the code from the Wiki to achieve this task. All the code you need for this is given below (of course you’d first have to properly install CMB). Add this code to your theme functions.php file and you are good to go.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
<?php /** * CMB Tabbed Theme Options * * @author Arushad Ahmed <@dash8x, [email protected]> * @link https://arushad.com/how-to-create-a-tabbed-options-page-for-your-wordpress-theme-using-cmb * @version 0.1.0 */ class my_Admin { /** * Default Option key * @var string */ private $key = 'my_options'; /** * Array of metaboxes/fields * @var array */ protected $option_metabox = array(); /** * Options Page title * @var string */ protected $title = ''; /** * Options Tab Pages * @var array */ protected $options_pages = array(); /** * Constructor * @since 0.1.0 */ public function __construct() { // Set our title $this->title = __( 'Theme Options', 'theme_textdomain' ); } /** * Initiate our hooks * @since 0.1.0 */ public function hooks() { add_action( 'admin_init', array( $this, 'init' ) ); add_action( 'admin_menu', array( $this, 'add_options_page' ) ); //create tab pages } /** * Register our setting tabs to WP * @since 0.1.0 */ public function init() { $option_tabs = self::option_fields(); foreach ($option_tabs as $index => $option_tab) { register_setting( $option_tab['id'], $option_tab['id'] ); } } /** * Add menu options page * @since 0.1.0 */ public function add_options_page() { $option_tabs = self::option_fields(); foreach ($option_tabs as $index => $option_tab) { if ( $index == 0) { $this->options_pages[] = add_menu_page( $this->title, $this->title, 'manage_options', $option_tab['id'], array( $this, 'admin_page_display' ) ); //Link admin menu to first tab add_submenu_page( $option_tabs[0]['id'], $this->title, $option_tab['title'], 'manage_options', $option_tab['id'], array( $this, 'admin_page_display' ) ); //Duplicate menu link for first submenu page } else { $this->options_pages[] = add_submenu_page( $option_tabs[0]['id'], $this->title, $option_tab['title'], 'manage_options', $option_tab['id'], array( $this, 'admin_page_display' ) ); } } } /** * Admin page markup. Mostly handled by CMB * @since 0.1.0 */ public function admin_page_display() { $option_tabs = self::option_fields(); //get all option tabs $tab_forms = array(); ?> <div class="wrap cmb_options_page <?php echo $this->key; ?>"> <h2><?php echo esc_html( get_admin_page_title() ); ?></h2> <!-- Options Page Nav Tabs --> <h2 class="nav-tab-wrapper"> <?php foreach ($option_tabs as $option_tab) : $tab_slug = $option_tab['id']; $nav_class = 'nav-tab'; if ( $tab_slug == $_GET['page'] ) { $nav_class .= ' nav-tab-active'; //add active class to current tab $tab_forms[] = $option_tab; //add current tab to forms to be rendered } ?> <a class="<?php echo $nav_class; ?>" href="<?php menu_page_url( $tab_slug ); ?>"><?php esc_attr_e($option_tab['title']); ?></a> <?php endforeach; ?> </h2> <!-- End of Nav Tabs --> <?php foreach ($tab_forms as $tab_form) : //render all tab forms (normaly just 1 form) ?> <div id="<?php esc_attr_e($tab_form['id']); ?>" class="group"> <?php cmb_metabox_form( $tab_form, $tab_form['id'] ); ?> </div> <?php endforeach; ?> </div> <?php } /** * Defines the theme option metabox and field configuration * @since 0.1.0 * @return array */ public function option_fields() { // Only need to initiate the array once per page-load if ( ! empty( $this->option_metabox ) ) { return $this->option_metabox; } $this->option_metabox[] = array( 'id' => 'general_options', //id used as tab page slug, must be unique 'title' => 'General Options', 'show_on' => array( 'key' => 'options-page', 'value' => array( 'general_options' ), ), //value must be same as id 'show_names' => true, 'fields' => array( array( 'name' => __('Header Logo', 'theme_textdomain'), 'desc' => __('Logo to be displayed in the header menu.', 'theme_textdomain'), 'id' => 'header_logo', //each field id must be unique 'default' => '', 'type' => 'file', ), array( 'name' => __('Login Logo', 'theme_textdomain'), 'desc' => __('Logo to be displayed in the login page. (320x120)', 'theme_textdomain'), 'id' => 'login_logo', 'default' => '', 'type' => 'file', ), array( 'name' => __('Favicon', 'theme_textdomain'), 'desc' => __('Site favicon. (32x32)', 'theme_textdomain'), 'id' => 'favicon', 'default' => '', 'type' => 'file', ), array( 'name' => __( 'SEO', 'theme_textdomain' ), 'desc' => __( 'Search Engine Optimization Settings.', 'theme_textdomain' ), 'id' => 'branding_title', //field id must be unique 'type' => 'title', ), array( 'name' => __('Site Keywords', 'theme_textdomain'), 'desc' => __('Keywords describing this site, comma separated.', 'theme_textdomain'), 'id' => 'site_keywords', 'default' => '', 'type' => 'textarea_small', ), ) ); $this->option_metabox[] = array( 'id' => 'social_options', 'title' => 'Social Media Settings', 'show_on' => array( 'key' => 'options-page', 'value' => array( 'social_options' ), ), 'show_names' => true, 'fields' => array( array( 'name' => __('Facebook Username', 'theme_textdomain'), 'desc' => __('Username of Facebook page.', 'theme_textdomain'), 'id' => 'facebook', 'default' => '', 'type' => 'text' ), array( 'name' => __('Twitter Username', 'theme_textdomain'), 'desc' => __('Username of Twitter profile.', 'theme_textdomain'), 'id' => 'twitter', 'default' => '', 'type' => 'text' ), array( 'name' => __('Youtube Username', 'theme_textdomain'), 'desc' => __('Username of Youtube channel.', 'theme_textdomain'), 'id' => 'youtube', 'default' => '', 'type' => 'text' ), array( 'name' => __('Flickr Username', 'theme_textdomain'), 'desc' => __('Username of Flickr profile.', 'theme_textdomain'), 'id' => 'flickr', 'default' => '', 'type' => 'text' ), array( 'name' => __('Google+ Profile ID', 'theme_textdomain'), 'desc' => __('ID of Google+ profile.', 'theme_textdomain'), 'id' => 'google_plus', 'default' => '', 'type' => 'text' ), ) ); $this->option_metabox[] = array( 'id' => 'advanced_options', 'title' => 'Advanced Settings', 'show_on' => array( 'key' => 'options-page', 'value' => array( 'advanced_options' ), ), 'show_names' => true, 'fields' => array( array( 'name' => __('Color Scheme', 'theme_textdomain'), 'desc' => __('Main theme color.', 'theme_textdomain'), 'id' => 'color_scheme', 'default' => '', 'type' => 'colorpicker', ), array( 'name' => __('Custom CSS', 'theme_textdomain'), 'desc' => __('Enter any custom CSS you want here.', 'theme_textdomain'), 'id' => 'new_custom_css', 'default' => '', 'type' => 'textarea', ), ) ); //insert extra tabs here return $this->option_metabox; } /** * Returns the option key for a given field id * @since 0.1.0 * @return array */ public function get_option_key($field_id) { $option_tabs = $this->option_fields(); foreach ($option_tabs as $option_tab) { //search all tabs foreach ($option_tab['fields'] as $field) { //search all fields if ($field['id'] == $field_id) { return $option_tab['id']; } } } return $this->key; //return default key if field id not found } /** * Public getter method for retrieving protected/private variables * @since 0.1.0 * @param string $field Field to retrieve * @return mixed Field value or exception is thrown */ public function __get( $field ) { // Allowed fields to retrieve if ( in_array( $field, array( 'key', 'fields', 'title', 'options_pages' ), true ) ) { return $this->{$field}; } if ( 'option_metabox' === $field ) { return $this->option_fields(); } throw new Exception( 'Invalid property: ' . $field ); } } // Get it started $my_Admin = new my_Admin(); $my_Admin->hooks(); /** * Wrapper function around cmb_get_option * @since 0.1.0 * @param string $key Options array key * @return mixed Option value */ function my_option( $key = '' ) { global $my_Admin; return cmb_get_option( $my_Admin->get_option_key($key), $key ); } ?> |
Once you have added the code you should be left with an options page with 3 tabs like the one shown below.
You should be able to access your custom saved theme options by using my_option( option_id ); inside your theme files.
Adding New Tabs
If you want to add new tabs to the options page all you have to do is to add extra tab arrays inside the option_fields() function. The below example will add a tab called New Tab with a single text field.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$this->option_metabox[] = array( 'id' => 'new_tab', //must be unique 'title' => 'New Tab', 'show_on' => array( 'key' => 'options-page', 'value' => array( 'new_tab' ), ), //IMPORTANT!: value must be same as tab id 'show_names' => true, 'fields' => array( array( 'name' => __('New Text Field', 'theme_textdomain'), 'desc' => __('New Example Text Field.', 'theme_textdomain'), 'id' => 'text_field_id', //must be unique 'default' => '', 'type' => 'text' ), ) ); |
Remember that the tabs will be rendered in the order that you add them. Also each tab id and field id must be unique. Most importantly the tab id and value inside 'show_on' => array( 'key' => 'options-page', 'value' => array( 'new_tab' ), ), must be exactly same.
You can add extra fields by adding more arrays inside the fields array. For a list of available field types you can check here.
Notes
If you set a default value for a field, when you try to save a blank value for the particular field, it would change back to the default value. So if you want to allow blank values for any field, set an empty string as the default value.
In future versions I would like to add a “reset to defaults” button to the options page which would make it easier for theme users to reset the theme if they mess up the settings.
21 Comments
Hi Arushad,
that is this is really a great work! I have CMB installed correctly and it works for my Custom Post Type and the original example of the Theme Options Page, but unfortunately your code will not work for me.
I am using WordPress 4.0 and CMB2 and have copied your code and pasted without further changes. The menu item and the page with the tabs are displayed correctly, but the individual metaboxes are not. What am I doing wrong? Do you have any idea?
Thanks in advance
Peter
Hi Peter,
Are the metaboxes not being displayed at all on the options page?
Where are you adding the code? Try putting this code in a separate file and include it from functions.php right after the CMB library files are included.
Can’t access custom saved theme options by using my_option( option_id );
My bad it is working! I wrapped it under if is admin
Nice work Arushad!
Do you have an example of how to display an image upload in your theme? I use CMB2 as well for custom post types, but I can’t use the same logic here apparently to display images.
Any thoughts?
I am unfamiliar on how to display a saved theme option by using my_option( option_id ); Can you please explain how to use this and how it calls a specific metabox? I have looked around and can not figure this out
Have you tested to see that the options panel is working and saving data within WordPress first? If it’s working on the admin side, then just use something like below to display in your theme. I’ll keep it in context of the demo in this article so you can see the relationship.
This example would get the “YouTube” option data:
Also make sure that you’re using the correct call to retrieve the theme option. It should match the function name that is on line 290 of the provided example – if you’ve modified that, make sure to update this. ‘youtube’ is the ID found on line 193.
Hope this helps.
sorry – php was stripped from my comment:
my_option(‘youtube’);
How to create same theme option page by using https://wordpress.org/plugins/cmb2/ Plugins , it’s upgraded version of CMB.
Hi Arushad,
Thanks a lot about this article !
unfortunately i can’t use this code with CM2 group fields .
is it match with CM2 group repeatable fields ?
thanks !
Do this: array(
‘id’ => ‘tp_ads’,
‘type’ => ‘group’,
‘options’ => array(
‘group_title’ => esc_html__( ‘Entry {#}’, ‘cmb2’ ), // {#} gets replaced by row number
‘add_button’ => esc_html__( ‘Add New’, ‘cmb2’ ),
‘remove_button’ => esc_html__( ‘Remove’, ‘cmb2’ ),
‘sortable’ => true, // beta
// ‘closed’ => true, // true to have the groups closed by default
),
‘fields’ => array(
array(
‘name’ => esc_html__( ‘Banner Image’, ‘cmb2’ ),
‘desc’ => esc_html__( ‘Upload an image or enter a URL.’, ‘cmb2’ ),
‘id’ => ‘ad_imagey’,
‘type’ => ‘file’,
),
array(
‘name’ => esc_html__( ‘Banner Image’, ‘cmb2’ ),
‘desc’ => esc_html__( ‘Upload an image or enter a URL.’, ‘cmb2’ ),
‘id’ => ‘ad_imagex’,
‘type’ => ‘file’,
),
),
),
Thank you. Seems to work with CMB2 with some minor adjustments.
I haven’t tried all field types, but your setup above is working as expected.
would u please update with cmb2.
works on cmb2 too, just need to change cmb_metabox_form( $tab_form, $tab_form[‘id’] ); to cmb2_metabox_form( $tab_form, $tab_form[‘id’] );
and cmb_get_option to cmb2_get_option
how do you echo out the fields to the wordpress template
awesome. thanks for this. saved me a lot of time. This should be included in the examples!
For those following along, there is now a snippet in the snippet library for adding tabbed navigation for options-pages : https://github.com/CMB2/CMB2-Snippet-Library/blob/master/options-and-settings-pages/options-pages-with-tabs-and-submenus.php
And even better, the ‘tab_group’ box property will be in the next version of CMB2 to make things easier: https://github.com/CMB2/CMB2/commit/5a4c7fb319a71f7232a17bc4ce30ae10d5b32190
Thank’s man! You have this much better documented then CMB2 😉