This tutorial will show you how to create a collapsible fixed pop-up. 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 fewer 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 programmatically add it to my templates based on various criteria, such as categories or tags.