npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@stormid/cookie-banner

v1.0.0-alpha.32

Published

GDPR compliant cookie banner

Downloads

618

Readme

Cookie banner

GDPR compliant cookie banner and consent form.

Renders a cookie banner and a consent form based on configuration settings, and conditionally invokes cookie-reliant functionality based on user consent.


Usage

Cookie consent is based on categorising cookies and the functions that initialise them, describing them in a configuration object passed into the module at initialisition.

The cookie banner renders itself if no consent preferences are recorded in the browser.

The consent form renders into a DOMElement with a particular className configurable options (classNames.formContainer).

A page containing a cookie consent form should include a visually hidden live region (role=alert) with a particular className (classNames.formAnnouncement), default: 'privacy-banner__form-announcement'.

Optionally the banner also supports basic Google EU consent mode [https://developers.google.com/tag-platform/security/guides/consent?consentmode=basic], and can push user consent preferences to the dataLayer for Google libraries to use. All that is necessary to suport Google consent mode is to map Google consent categories to the cookie categories in the configuration.

For example, to map the ad_storage, ad_user_data, and ad_personalisation to an 'ads' consent category defined in the banner config, add a euConsentTypes object to the configuration like this:

euConsentTypes: {
    ad_storage: 'test',
    ad_user_data: 'test',
    ad_personalization: 'test'
}

Install the package

npm i -S @stormid/cookie-banner

Create a container element for the consent form.

<div class="privacy-banner__form-container"></div>

Create a visually hidden live region for the screen reader announcement.

<div class="visually-hidden privacy-banner__form-announcement" role="alert"></div>

Initialise the module (example configuration shown below)

import banner from '@stormid/cookie-banner';

const cookieBanner = banner({
    types: {
        'performance': {
            suggested: true, //set as pre-checked on consent form as a suggested response
            title: 'Performance preferences',
            description: 'Performance cookies are used to measure the performance of our website and make improvements. Your personal data is not identified.',
            labels: {
                yes: 'Pages you visit and actions you take will be measured and used to improve the service',
                no: 'Pages you visit and actions you take will not be measured and used to improve the service'
            },
            fns: [
                state => { 
                    //function that depends upon or creates a 'performance' cookie
                },
                state => state.utils.gtmSnippet(<UA-CODE>)
            ]
        },
        'thirdParty': {
            title: 'Third party preferences',
            description: 'We work with third party partners to show you ads for our products and services across the web, and to serve video and audio content.  You can choose whether we collect and share that data with our partners below. ',
            labels: {
                yes: 'Our partners might know you have visited our website',
                no: 'Our partners will will not know you have visited out website but you cannot video third party video and audio content'
            },
            fns: [
                model => { 
                    //function that depends upon or creates a 'performance' cookie
                },
                state => state.utils.renderIframe(),
                state => state.utils.gtmSnippet(<UA-CODE>)
            ]
        }
    }
});

Options

{
    name: '.CookiePreferences', //name of the cookie set to record user consent
    path: '/', //path of the preferences cookie
    domain: window.location.hostname === 'localhost' ? '' : `.${removeSubdomain(window.location.hostname)}`, //domain of the preferences cookie, defaults to .<root-domain>
    secure: true, //preferences cookie secure
    samesite: 'lax', //preferences cookie samesite
    expiry: 365, //preferences cookie expiry in days
    types: {}, //types of cookie-dependent functionality
    euConsentTypes: {}, //map Google EU consent categories to types of cookie defined in 'types'
    necessary: [], //cookie-dependent functionality that will always execute, for convenience only
    policyURL: '/cookie-policy#preferences', //URL to cookie policy page (location of cookie consent form) rendered in the banner
    classNames: {
        banner: 'privacy-banner',
        acceptBtn: 'privacy-banner__accept',
        rejectBtn: 'privacy-banner__reject',
        submitBtn: 'privacy-banner__submit',
        field: 'privacy-banner__field',
        form: 'privacy-banner__form',
        fieldset: 'privacy-banner__fieldset',
        legend: 'privacy-banner__legend',
        formContainer: 'privacy-banner__form-container', //where the form is rendered
        formMessage: 'privacy-banner__form-msg',
        formAnnouncement: 'privacy-banner__form-announcement', //screen reader announcement
        title: 'privacy-banner__form-title',
        description: 'privacy-banner__form-description'
    },
    hideBannerOnFormPage: false, //don't show the banner when the user is on the same page as a consent form
    savedMessage: 'Your settings have been saved.', //displayed after consent form update,
    trapTab: false, //trap the user's keyboard tab within the banner when open
    bannerTemplate(model){
        return `<section role="dialog" aria-live="polite" aria-label="Your privacy" class="${model.classNames.banner}">
            <div class="privacy-content">
                <div class="wrap">
                    <!--googleoff: all-->
                    <div class="privacy-banner__title">Cookies</div>
                    <p>We use cookies to improve your experience on our site and show you personalised advertising.</p>
                    <p>Find out more from our <a class="privacy-banner__link" rel="noopener noreferrer nofollow" href="/privacy-policy">privacy policy</a> and <a class="privacy-banner__link" rel="noopener noreferrer nofollow" href="${model.policyURL}">cookie policy</a>.</p>
                    <button class="btn btn--primary ${model.classNames.acceptBtn}">Accept and close</button>
                    <a class="privacy-banner__link" rel="noopener noreferrer nofollow" href="${model.policyURL}">Your options</a>
                    <!--googleon: all-->
                </div>
            </div>
        </section>`;
    },
    messageTemplate(model){
        return `<div class="${model.settings.classNames.formMessage}" aria-role="alert">${model.settings.savedMessage}</div>`
    },
    formTemplate(model){
        return `<form id="preferences" class="${model.settings.classNames.form}" novalidate>
                ${Object.keys(model.settings.types).map(type => `<fieldset class="${model.settings.classNames.fieldset}">
                <legend class="${model.settings.classNames.legend}">
                    <span class="${model.settings.classNames.title}">${model.settings.types[type].title}</span>
                    <span class="${model.settings.classNames.description}">${model.settings.types[type].description}</span>
                </legend>
                <div class="form-row">
                    <div class="relative">
                        <label class="privacy-banner__label">
                            <input
                                class="${model.settings.classNames.field}"
                                type="radio"
                                name="privacy-${type.split(' ')[0].replace(' ', '-')}"
                                value="1"
                                ${model.consent[type] === 1 ? ` checked` : ''}>
                            <span class="privacy-banner__label-text">I am OK with this</span>
                            <span class="privacy-banner__label-description">${model.settings.types[type].labels.yes}</span>
                        </label>    
                    </div>
                </div>
                <div class="form-row">
                    <div class="relative">
                        <label class="privacy-banner__label">
                            <input
                                class="${model.settings.classNames.field}"
                                type="radio"
                                name="privacy-${type.split(' ')[0].replace(' ', '-')}"
                                value="0"
                                ${model.consent[type] === 0 ? ` checked` : ''}>
                            <span class="privacy-banner__label-text">No thank you</span>
                            <span class="privacy-banner__label-description">${model.settings.types[type].labels.no}</span>
                        </label>    
                    </div>
                </div>
            </fieldset>`).join('')}
            <button class="${model.settings.classNames.submitBtn}"${Object.keys(model.consent).length === 0 ? ` disabled` : ''}>Save my settings</button>
        </form>`;
    }
}

Utility functions

There are two utility functions provided by the library designed to be invoked following user consent.

Render iframe

state.utils.renderIframe

Renders an iframe from a placeholder element with specific data attributes:

<div data-iframe-src="https://www.youtube.com/embed/qpLKTUQev30" data-iframe-title="Test video" data-iframe-height="1600px" data-iframe-width="900px">
    <p>Update your cookie preferences to view this content</p>
    <button class="js-preferences-update">Update</button>
</div>

In the cookie banner configuration:

import cookieBanner from '@stormid/cookie-banner';

cookieBanner({
    ...lots of other config
    type: {
        thirdParty: [
            state => state.utils.renderIframe()
        ]
    }
})

Google Tag Manager Snippet

state.utils.gtmSnippet

Invokes a GTM snippet to load the GTM library via an script element, just pass the Tag Manager ID/UA number as an argument

In the cookie banner configuration:

import cookieBanner from '@stormid/cookie-banner';

cookieBanner({
    ...lots of other config
    type: {
        thirdParty: [
            state => state.utils.gtmSnippet(`UA-1234-5678`)
        ]
    }
})

API

The Object returned from initialisation exposes the interface

{
    getState, Function that returns the current state Object
    showBanner, Function to show the banner, accepts a callback function
    renderForm, Function to render the consent form
}

Events

There are three custom events that an instance of the cookie banner dispatches:

  • banner.show when the banner is displayed
  • banner.hide when it is hidden
  • banner.consent when consent is set or updated

The events are dispatched on the document. A reference to the getState function of the instance is contained in the custom event detail.

const instance = banner(options);

document.addEventListener('banner.show', e => {
    //e.g. initialise toggle for form-in-banner implementation
    const [ bannerToggle ] = toggle('.js-banner-toggle'); 
    const state = e.detail.getState();
    // do something with state if we want to
});

Tests

npm t

License

MIT