How to Create a Fixed Teaser Block in WordPress

πŸ‘‹ Ciprian on Tuesday, April 27, 2021 in Blog, Featured
Last modified on Sunday, April 25, 2021

Learn JavaScript by example. Code snippets, how-to's and tutorials. Try now!

Whiskey Teaser Block
Whiskey Teaser Block

This tutorial will show you how to create a collapsible fixed popup. It can contain subscriptions forms, contact forms, calls to action, and more. You can see a demo of this feature, right here, on this page.

Based on the amount of code below, this looks like an advanced WordPress tutorial, but it’s not. Trust me. After you go through each file, it will make a lot of sense. Let’s start!

Step 1

Add the following code to your theme’s functions.php (or another template/partials file, depending on your theme’s structure):

functions.php Blocks

Let’s call the teaser block (which is actually a shortcode, because I need it to be accessible via PHP, as well, using the do_shortcode() function) and the actual editor block:

<?php
/**
 * Templates, template variables and block patterns
 */
require_once 'blocks.php';
require_once 'blocks/teaser/index.php';

Step 2

Next, let’s take each file, one by one. The teaser shortcode first, which uses a reusable block for the content. Replace 123 with your own reusable block ID.

blocks.php Template

<?php
function whiskey_teaser($atts) {
    $attributes = shortcode_atts([
        'id' => 0,
        'width' => 400,
        'trigger' => '',
        'controls' => 1,
        'accent' => '#333',
        'title' => 'Get Started'
    ], $atts);

    $title = (string) sanitize_text_field($attributes['title']);
    $width = (int) sanitize_text_field($attributes['width']);
    $trigger = sanitize_text_field($attributes['trigger']);
    $controls = (int) sanitize_text_field($attributes['controls']);
    $accent = (string) sanitize_text_field($attributes['accent']);
    $reusable_block_id = 123;
    $reusable_block_content = '';
    $reusable_block_checkbox = '';

    $reusable_block = get_post($reusable_block_id);
    $reusable_block_content = apply_filters('the_content', $reusable_block->post_content);

    if ($controls === 1) {
        $reusable_block_checkbox = '<p><small><input type="checkbox" name="teaser_nag" id="whiskey-teaser--nag" value="1"> <label for="whiskey-teaser--nag">Don\'t show this again.</label></small></p>';
    }

    $out = '<section id="whiskey-teaser--fixed-container" style="width: ' . $width . 'px;" data-trigger="' . $trigger . '">
        <div class="whiskey-teaser--top">
            <div class="whiskey-teaser--title" style="background-color: ' . $accent . ';">' . $title . '</div>
            <div id="whiskey-teaser--close" class="collapsed-icon" style="background-color: ' . $accent . ';"><svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="svg-inline--fa fa-chevron-double-down fa-w-14" data-icon="chevron-double-down" data-prefix="far" viewBox="0 0 448 512"><defs/><path fill="currentColor" d="M441.9 89.7L232.5 299.1a12 12 0 01-17 0L6.1 89.7a12 12 0 010-17l19.8-19.8a12 12 0 0117 0L224 233.6 405.1 52.9a12 12 0 0117 0l19.8 19.8a12 12 0 010 17zm0 143l-19.8-19.8a12 12 0 00-17 0L224 393.6 42.9 212.9a12 12 0 00-17 0L6.1 232.7a12 12 0 000 17l209.4 209.4a12 12 0 0017 0l209.4-209.4a12 12 0 000-17z"/></svg></div>
        </div>

        <div class="whiskey-teaser--body" style="border-color: ' . $accent . ';">
            ' . $reusable_block_content . '
            ' . $reusable_block_checkbox . '
        </div>
    </section>';

    return $out;
}
add_shortcode('whiskey-teaser', 'whiskey_teaser');

Next is the editor block. Note that I am creating my own Whiskey category:

blocks/teaser/index.php Block

<?php
add_action('init', 'whiskey_teaser_fixed_block_main');

function whiskey_block_categories($categories, $post) {
    return array_merge(
        $categories, [
            [
                'slug' => 'whiskey',
                'title' => 'Whiskey',
                'icon' => 'star-filled',
            ]
        ]
    );
}
add_filter('block_categories', 'whiskey_block_categories', 10, 2);

function whiskey_teaser_fixed_enqueue_block_editor_assets() {
    wp_enqueue_script(
        'whiskey-teaser-fixed-block-script',
        get_stylesheet_directory_uri() . '/blocks/teaser/teaser-block.js',
        ['wp-blocks', 'wp-element', 'wp-i18n', 'wp-editor', 'wp-components']
    );
}

add_action('enqueue_block_editor_assets', 'whiskey_teaser_fixed_enqueue_block_editor_assets');

function whiskey_teaser_fixed_block_main() {
    if (!function_exists('addShortcodeParam')) {
        function addShortcodeParam($key, $value) {
            if (trim($value) == '') {
                return '';
            }
            $asc = $key . '="' . $value . '" ';

            return $asc;
        }
    }

    function whiskey_teaser_fixed_render($attributes, $content) {
        $out = '';

        // Reusable Block ID
        $teaser_id = sanitize_text_field($attributes['teaser_id']);

        // Teaser Title
        $title = sanitize_text_field($attributes['title']);

        // Teaser Trigger
        $trigger = sanitize_text_field($attributes['trigger']);

        // Teaser Accent Colour
        $teaser_accent = sanitize_text_field($attributes['teaser_accent']);

        // Controls
        $controls = (1 !== (int) $attributes['controls']) ? 0 : 1;

        // Teaser Width
        $teaser_width = (int) trim($attributes['teaser_width']);
        $teaser_width = ($teaser_width > 0) ? $teaser_width : 400;

        $shortcodeData = '[whiskey-teaser ';
        $shortcodeData .= addShortcodeParam('controls', $controls);
        $shortcodeData .= addShortcodeParam('trigger', $trigger);
        $shortcodeData .= addShortcodeParam('accent', $teaser_accent);
        $shortcodeData .= addShortcodeParam('title', $title);
        $shortcodeData .= addShortcodeParam('width', $teaser_width);
        $shortcodeData .= 'id="' . $teaser_id . '"]';

        $out .= $shortcodeData;

        return $out;
    }

    register_block_type('whiskey/whiskey-teaser-fixed', [
        'render_callback' => 'whiskey_teaser_fixed_render',
        'attributes' => [
            'teaser_id' => [
                'type' => 'string',
                'default' => '0',
            ],
            'title' => [
                'type' => 'string',
                'default' => 'Get Started',
            ],
            'trigger' => [
                'type' => 'string',
                'default' => '',
            ],
            'controls'=> [
                'type' => 'boolean',
                'default' => true,
            ],
            'teaser_width' => [
                'type' => 'string',
                'default' => '400',
            ],
            'teaser_accent' => [
                'type' => 'string',
                'default' => '',
            ],
        ],
    ]);
}

Next is the actual editor block. I have opted for vanilla JavaScript for better readability and less dependencies.

/blocks/teaser/teaser-block.js

(function(editor, components, i18n, element) {
    var el = element.createElement;
    var registerBlockType = wp.blocks.registerBlockType;

    var InspectorControls = wp.editor.InspectorControls;
    var TextControl = wp.components.TextControl;
    var TextareaControl = wp.components.TextareaControl;
    var RangeControl = wp.components.RangeControl;
    var ServerSideRender = wp.components.ServerSideRender;
    var RadioControl = wp.components.RadioControl;
    var MenuItemsChoice  = wp.components.MenuItemsChoice;
    var ToggleControl = wp.components.ToggleControl;

    var PanelColorSettings = wp.editor.ColorPanelSettings;
    var ColorPalette = wp.components.ColorPalette;
    var ColorPicker = wp.components.ColorPicker;
    //var withColors = wp.editor.withColors;

    registerBlockType('whiskey/whiskey-teaser-fixed', {
        title: 'Whiskey Teaser',
        description: 'A full-featured whiskey block.',
        icon: 'slides',
        category: 'whiskey',
        attributes: {
            teaser_id: {
                type: 'string',
                default: '0',
            },
            title: {
                type: 'string',
                default: 'Get Started',
            },
            trigger: {
                type: 'string',
                default: '',
            },
            controls: {
                type: 'boolean',
                default: true,
            },
            teaser_width: {
                type: 'string',
                default: '400',
            },
            teaser_accent: {
                type: 'string',
                default: '',
            },
        },

        edit: function (props) {
            var attributes = props.attributes;

            var teaser_id = attributes.teaser_id;
            var title = attributes.title;
            var trigger = attributes.trigger;
            var controls = attributes.controls;
            var teaser_width = attributes.teaser_width;
            var teaser_accent = attributes.teaser_accent;

            return [
                el(InspectorControls, { key: 'inspector' },
                    el(
                        components.PanelBody, {
                            title: 'Teaser Settings',
                            className: 'whiskey_block',
                            initialOpen: true,
                        },

                        el(ToggleControl, {
                            type: 'boolean',
                            label: 'Controls',
                            help: 'Whether to show teaser controls ("Close" checkbox) by default.',
                            checked: !!controls,
                            onChange: function (new_controls) {
                                props.setAttributes({ controls: new_controls });
                            },
                        }),
                        el(TextControl, {
                            type: 'string',
                            label: 'Teaser ID',
                            placeholder: i18n.__('0'),
                            help: i18n.__('Teaser reusable block ID'),
                            value: teaser_id,
                            onChange: function (new_teaser_id) {
                                props.setAttributes({ teaser_id: new_teaser_id });
                            },
                        }),
                        el(TextControl, {
                            type: 'string',
                            label: 'Teaser Title',
                            placeholder: i18n.__('Get Started'),
                            help: i18n.__('Teaser title'),
                            value: title,
                            onChange: function (new_title) {
                                props.setAttributes({ title: new_title });
                            },
                        }),
                        el(TextControl, {
                            type: 'string',
                            label: 'Teaser Trigger',
                            placeholder: i18n.__(''),
                            help: i18n.__('Clicking an element with this class or ID will minimise/collapse the teaser on subsequent pageviews.'),
                            value: trigger,
                            onChange: function (new_trigger) {
                                props.setAttributes({ trigger: new_trigger });
                            },
                        }),
                        el(TextControl, {
                            type: 'string',
                            label: 'Teaser width, in pixels (default is 400)',
                            placeholder: '600',
                            value: teaser_width,
                            onChange: function (new_teaser_width) {
                                props.setAttributes({ teaser_width: new_teaser_width });
                            },
                        }),
                        el(ColorPalette, {
                            /*
                            colors: [
                                { name: 'red', color: '#f00' },
                                { name: 'white', color: '#fff' },
                                { name: 'blue', color: '#00f' },
                            ],
                            /**/
                            type: 'string',
                            label: 'Color',
                            placeholder: '',
                            value: teaser_accent,
                            onChange: function (new_teaser_accent) {
                                props.setAttributes({ teaser_accent: new_teaser_accent });
                            },
                        }),
                        /*
                        el(ColorPicker, {
                            type: 'string',
                            label: 'Color',
                            placeholder: '',
                            value: teaser_accent,
                            onChange: function (new_teaser_accent) {
                                props.setAttributes({ teaser_accent: new_teaser_accent });
                            },
                        }),
                        /**/
                    ),
                ),
                el(ServerSideRender, {
                    block: 'whiskey/whiskey-teaser-fixed',
                    attributes: props.attributes
                })
            ];
        },

        save: function () {
            return null;
        },
    });
})(
    window.wp.editor,
    window.wp.components,
    window.wp.i18n,
    window.wp.element,
);

Next is the appearance and the responsive styles:

CSS

#whiskey-teaser--fixed-container {
    position: fixed;
    z-index: 256;
    bottom: 0;
    right: 5%;

    width: 400px;
}
.whiskey-teaser--top {
    position: relative;
    display: flex;
}
.whiskey-teaser--title {
    font-size: 1em;
    font-weight: 700;
    border-radius: 3px 3px 0 0;
    padding: 8px 16px;
    background-color: #333333;
    color: #fff;
    display: inline-block;
    box-shadow: 0 0 8px rgb(0 0 0 / 25%);
}
.whiskey-teaser--title.collapsed {
    top: 1px;
    position: relative;
}
#whiskey-teaser--close {
    font-size: 1em;
    cursor: pointer;
    margin-left: auto;
    border-radius: 3px 3px 0 0;
    padding: 8px 16px;
    background-color: #333333;
    color: #fff;
    display: inline-block;
    box-shadow: 0 0 8px rgb(0 0 0 / 25%);
}
#whiskey-teaser--close svg {
    transition: transform 0.1s cubic-bezier(0.65, 0, 0.35, 1);
    transition-delay: 0.5s;
}
#whiskey-teaser--close.collapsed-icon svg {
    transform: rotateX(0deg);
}
#whiskey-teaser--close.expanded-icon svg {
    transform: rotateX(180deg);
}


.whiskey-teaser--body {
    position: relative;
    z-index: calc(256 + 1);
    background: white;
    padding: 16px 24px;
    border: 1px solid #333333;
    border-bottom-width: 0;
    box-shadow: 0 0 8px rgb(0 0 0 / 25%);
    overflow-y: scroll;
    width: 100%;
    max-height: 80vh;

    transition: all 0.6s cubic-bezier(0.65, 0, 0.35, 1);
}
.whiskey-teaser--body.minimised {
    max-height: 128px;
}

.whiskey-teaser--body::-webkit-scrollbar {
    -webkit-appearance: none;
    appearance: none;
    width: 5px;
}
.whiskey-teaser--body::-webkit-scrollbar-thumb {
    border-radius: 0;
    background-color: rgba(0, 0, 0, .5);
    box-shadow: 0 0 1px rgba(255, 255, 255, .5);
}



@media only screen and (max-width: 768px) {
    #whiskey-teaser--fixed-container {
        position: fixed;
        bottom: 0px;
        right: 0;
        width: 100% !important;
        margin: 0;
        padding: 16px 16px 0 16px;
    }
}

The final functionality is collapsing/expanding the teaser and collapsing it (and saving the state) if the “Don’t show this again” has been ticked. I am using localStorage for this.

document.addEventListener('DOMContentLoaded', () => {
    // Whiskey teaser
    if (document.getElementById('whiskey-teaser--close')) {
        if (localStorage.getItem('whiskey-teaser') === 'minimised') {
            document.getElementById('whiskey-teaser--nag').checked = true;

            document.querySelector('.whiskey-teaser--title').classList.add('collapsed');
            document.querySelector('.whiskey-teaser--body').classList.add('minimised');
        }

        document.getElementById('whiskey-teaser--close').addEventListener('click', () => {
            document.getElementById('whiskey-teaser--close').classList.toggle('expanded');
            document.getElementById('whiskey-teaser--close').classList.toggle('expanded-icon');
            document.getElementById('whiskey-teaser--close').classList.toggle('collapsed-icon');

            document.querySelector('.whiskey-teaser--title').classList.toggle('collapsed');
            document.querySelector('.whiskey-teaser--body').classList.toggle('minimised');
        });

        document.getElementById('whiskey-teaser--nag').addEventListener('click', () => {
            if (document.getElementById('whiskey-teaser--nag').checked) {
                document.querySelector('.whiskey-teaser--title').classList.add('collapsed');
                document.querySelector('.whiskey-teaser--body').classList.add('minimised');

                localStorage.setItem('whiskey-teaser', 'minimised');
            } else {
                localStorage.setItem('whiskey-teaser', '');
            }
        });

        // Optional trigger click
        if (document.getElementById('whiskey-teaser--fixed-container').dataset.trigger !== '') {
            let trigger = document.getElementById('whiskey-teaser--fixed-container').dataset.trigger;

            document.querySelector(trigger).addEventListener('click', () => {
                localStorage.setItem('whiskey-teaser', 'minimised');
            });
        }
    }
}, false);

The back-end is a mix of both worlds: a shortcode and a block. A sample shortcode would look like this:

[whiskey-teaser controls="1" trigger=".no-trigger" accent="#b53471" title="Whiskey Teaser Demo" width="400" id="106131"]

I can use the do_shortcode() function and programatically add it to my templates based on various criteria, such as categories or tags.

Whiskey Teaser Demo

This is a Whiskey Teaser demo!

Do you like to see WordPress tutorials? Or maybe you prefer WordPress plugins? What about WordPress themes?

Well, take your pick:

πŸ‘‹ Added by Ciprian on Tuesday, April 27, 2021 in Blog, Featured. Last modified on Sunday, April 25, 2021.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Privacy Policy