A content locker hides a small piece of your page, a download link, a coupon code, a cheat sheet, until the visitor does something for you in return, like sharing the page or joining your newsletter. It is a fair trade: they get the goodies, you get the reach.

Most lockers you'll find are paid WordPress plugins stuffed with tracking scripts. Ours is a couple hundred lines of plain HTML, CSS, and JavaScript, only about 50 of which are script: no jQuery, no icon fonts, no third-party anything. It blurs the locked content, unlocks on a share or an email signup, and remembers the unlock so returning visitors aren't nagged twice.

Quick answer

Wrap your content in a container with an overlay on top. CSS blurs the locked content with filter: blur(), and JavaScript opens the share dialog in a popup with window.open(), watches for that window to close, then reveals the content and saves the unlock in localStorage so it stays open on future visits.

1. How a Content Locker Works

The whole trick is two layers stacked on top of each other. The real content sits at the bottom, and an overlay sits above it asking the visitor to earn their way in. Four things happen in order:

  1. Lock. CSS blurs the content and lays the overlay with the share buttons on top.
  2. Action. The visitor clicks a share button, which opens the network's share dialog in a small popup.
  3. Verify. JavaScript watches the popup and treats it closing as the share being finished.
  4. Unlock. The overlay fades out, the blur melts away, and the unlock is saved to localStorage.
The locked state of the content locker with a blurred download card and six social share buttons
The locked state: the content is blurred underneath while the overlay asks for a share.

This tutorial was fully rewritten in 2026. The old version leaned on Font Awesome imports and had share buttons for Google+ and StumbleUpon, which tells you how overdue the rewrite was. Everything below is modern, dependency-free code you can drop into any site.

2. Creating the Locker HTML

Everything lives in one block of markup. Anything inside locker-content is what gets locked, so put your download button, coupon code, or whatever you're gating in there. The overlay sits right next to it, and the brand icons are embedded straight into the share buttons as inline SVG, so there is no icon font, sprite file, or CDN to load:

HTML
<div class="locker" data-locker="css-cheat-sheet">
    <!-- The content you want to lock goes in here -->
    <div class="locker-content">
        <h3>The Complete CSS Cheat Sheet (PDF)</h3>
        <p>Every selector, property, and shorthand on two printable pages. Keep it next to your keyboard and stop googling flexbox.</p>
        <a class="download-button" href="css-cheat-sheet.pdf" download>Download the PDF</a>
    </div>
    <!-- The overlay visitors see while the content is locked -->
    <div class="locker-overlay">
        <svg class="locker-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12,17C10.89,17 10,16.1 10,15C10,13.89 10.89,13 12,13A2,2 0 0,1 14,15A2,2 0 0,1 12,17M18,20V10H6V20H18M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V10C4,8.89 4.89,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"/></svg>
        <p class="locker-title">This content is locked</p>
        <p class="locker-hint">Share this page on any network below to unlock the download instantly.</p>
        <!-- Icons: Font Awesome Free 6 (fontawesome.com) - CC BY 4.0 -->
        <div class="locker-share">
            <a href="#" data-share="x" aria-label="Share on X">
                <svg viewBox="0 0 512 512" aria-hidden="true"><path d="M389.2 48h70.6L305.6 224.2 487 464H345L233.7 318.6 106.5 464H35.8L200.7 275.5 26.8 48H172.4L272.9 180.9 389.2 48zM364.4 421.8h39.1L151.1 88h-42L364.4 421.8z"/></svg>
            </a>
            <a href="#" data-share="facebook" aria-label="Share on Facebook">
                <svg viewBox="0 0 320 512" aria-hidden="true"><path d="M80 299.3V512H196V299.3h86.5l18-97.8H196V166.9c0-51.7 20.3-71.5 72.7-71.5c16.3 0 29.4 .4 37 1.2V7.9C291.4 4 256.4 0 236.2 0C129.3 0 80 50.5 80 159.4v42.1H14v97.8H80z"/></svg>
            </a>
            <a href="#" data-share="linkedin" aria-label="Share on LinkedIn">
                <svg viewBox="0 0 448 512" aria-hidden="true"><path d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.88-48.3 94 0 111.28 61.9 111.28 142.3V448z"/></svg>
            </a>
            <a href="#" data-share="pinterest" aria-label="Share on Pinterest">
                <svg viewBox="0 0 384 512" aria-hidden="true"><path d="M204 6.5C101.4 6.5 0 74.9 0 185.6 0 256 39.6 296 63.6 296c9.9 0 15.6-27.6 15.6-35.4 0-9.3-23.7-29.1-23.7-67.8 0-80.4 61.2-137.4 140.4-137.4 68.1 0 118.5 38.7 118.5 109.8 0 53.1-21.3 152.7-90.3 152.7-24.9 0-46.2-18-46.2-43.8 0-37.8 26.4-74.4 26.4-113.4 0-66.2-93.9-54.2-93.9 25.8 0 16.8 2.1 35.4 9.6 50.7-13.8 59.4-42 147.9-42 209.1 0 18.9 2.7 37.5 4.5 56.4 3.4 3.8 1.7 3.4 6.9 1.5 50.4-69 48.6-82.5 71.4-172.8 12.3 23.4 44.1 36 69.3 36 106.2 0 153.9-103.5 153.9-196.8C384 71.3 298.2 6.5 204 6.5z"/></svg>
            </a>
            <a href="#" data-share="reddit" aria-label="Share on Reddit">
                <svg viewBox="0 0 512 512" aria-hidden="true"><path d="M373 138.6c-25.2 0-46.3-17.5-51.9-41l0 0c-30.6 4.3-54.2 30.7-54.2 62.4l0 .2c47.4 1.8 90.6 15.1 124.9 36.3c12.6-9.7 28.4-15.5 45.5-15.5c41.3 0 74.7 33.4 74.7 74.7c0 29.8-17.4 55.5-42.7 67.5c-2.4 86.8-97 156.6-213.2 156.6S45.5 410.1 43 323.4C17.6 311.5 0 285.7 0 255.7c0-41.3 33.4-74.7 74.7-74.7c17.2 0 33 5.8 45.7 15.6c34-21.1 76.8-34.4 123.7-36.4l0-.3c0-44.3 33.7-80.9 76.8-85.5C325.8 50.2 347.2 32 373 32c29.4 0 53.3 23.9 53.3 53.3s-23.9 53.3-53.3 53.3zM157.5 255.3c-20.9 0-38.9 20.8-40.2 47.9s17.1 38.1 38 38.1s36.6-9.8 37.8-36.9s-14.7-49.1-35.7-49.1zM395 303.1c-1.2-27.1-19.2-47.9-40.2-47.9s-36.9 22-35.7 49.1c1.2 27.1 16.9 36.9 37.8 36.9s39.3-11 38-38.1zm-60.1 70.8c1.5-3.6-1-7.7-4.9-8.1c-23-2.3-47.9-3.6-73.8-3.6s-50.8 1.3-73.8 3.6c-3.9 .4-6.4 4.5-4.9 8.1c12.9 30.8 43.3 52.4 78.7 52.4s65.8-21.6 78.7-52.4z"/></svg>
            </a>
            <a href="#" data-share="whatsapp" aria-label="Share on WhatsApp">
                <svg viewBox="0 0 448 512" aria-hidden="true"><path d="M380.9 97.1C339 55.1 283.2 32 223.9 32c-122.4 0-222 99.6-222 222 0 39.1 10.2 77.3 29.6 111L0 480l117.7-30.9c32.4 17.7 68.9 27 106.1 27h.1c122.3 0 224.1-99.6 224.1-222 0-59.3-25.2-115-67.1-157zm-157 341.6c-33.2 0-65.7-8.9-94-25.7l-6.7-4-69.8 18.3L72 359.2l-4.4-7c-18.5-29.4-28.2-63.3-28.2-98.2 0-101.7 82.8-184.5 184.6-184.5 49.3 0 95.6 19.2 130.4 54.1 34.8 34.9 56.2 81.2 56.1 130.5 0 101.8-84.9 184.6-186.6 184.6zm101.2-138.2c-5.5-2.8-32.8-16.2-37.9-18-5.1-1.9-8.8-2.8-12.5 2.8-3.7 5.6-14.3 18-17.6 21.8-3.2 3.7-6.5 4.2-12 1.4-32.6-16.3-54-29.1-75.5-66-5.7-9.8 5.7-9.1 16.3-30.3 1.8-3.7.9-6.9-.5-9.7-1.4-2.8-12.5-30.1-17.1-41.2-4.5-10.8-9.1-9.3-12.5-9.5-3.2-.2-6.9-.2-10.6-.2-3.7 0-9.7 1.4-14.8 6.9-5.1 5.6-19.4 19-19.4 46.3 0 27.3 19.9 53.7 22.6 57.4 2.8 3.7 39.1 59.7 94.8 83.8 35.2 15.2 49 16.5 66.6 13.9 10.7-1.6 32.8-13.4 37.4-26.4 4.6-13 4.6-24.1 3.2-26.4-1.3-2.5-5-3.9-10.5-6.6z"/></svg>
            </a>
        </div>
    </div>
</div>

Two details do the heavy lifting here. The data-locker value is the locker's ID, so give every locker on a page its own unique value; the unlock is remembered under that name. Each share link carries a data-share attribute naming its network, which the JavaScript uses to build the share URL, so there is nothing to paste in or configure.

Every icon link also gets an aria-label, because a circle with an icon in it means nothing to a screen reader. The heading, text, and button inside the card are placeholder content; the demo page that styles them is included in the free download below.

3. Styling the Locker with CSS

The layout is one positioned card with the overlay absolutely stretched across it. The nice part is the lock effect itself: rather than hiding the content, we blur it with filter: blur(), which teases what's underneath and makes unlocking feel worthwhile.

CSS
/* Every knob you might want to tweak lives in one place */
.locker {
    --locker-radius: 12px;
    --locker-blur: 8px;
    --locker-speed: .4s;
    --locker-accent: #3e7bd6;
    --locker-overlay: linear-gradient(160deg, rgba(30, 41, 59, .92), rgba(15, 23, 42, .96));
    --locker-button: 44px;
    position: relative;
    max-width: 520px;
    border-radius: var(--locker-radius);
    overflow: hidden;
    box-shadow: 0 4px 24px rgba(15, 23, 42, .12);
}
/* The content being locked; the blur fades away smoothly on unlock */
.locker-content {
    padding: 30px;
    background-color: #fff;
    transition: filter var(--locker-speed) ease;
}
/* Blur the content and block clicks and text selection while it is locked */
.locker:not(.unlocked) .locker-content {
    filter: blur(var(--locker-blur));
    pointer-events: none;
    user-select: none;
}
/* The dark overlay that sits on top of the locked content */
.locker-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 20px;
    text-align: center;
    color: #fff;
    background: var(--locker-overlay);
    transition: opacity var(--locker-speed) ease, visibility var(--locker-speed);
}
/* Fade the overlay away once the locker is unlocked */
.locker.unlocked .locker-overlay {
    opacity: 0;
    visibility: hidden;
}
.locker-icon {
    width: 34px;
    height: 34px;
    fill: #94a3b8;
}
.locker-title {
    margin: 4px 0 0;
    font-size: 20px;
    font-weight: 700;
}
.locker-hint {
    margin: 0 0 10px;
    font-size: 14px;
    line-height: 1.5;
    color: #cbd5e1;
}
/* The row of round share buttons; each button reads its own --brand color */
.locker-share {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
}
.locker-share a {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--locker-button);
    height: var(--locker-button);
    border-radius: 50%;
    background-color: var(--brand);
    transition: transform .15s ease;
}
.locker-share a:hover,
.locker-share a:focus-visible {
    transform: scale(1.12);
}
.locker-share svg {
    width: 18px;
    height: 18px;
    fill: #fff;
}
/* The official brand color for each network */
[data-share=x] { --brand: #000000; }
[data-share=facebook] { --brand: #0866ff; }
[data-share=linkedin] { --brand: #0a66c2; }
[data-share=pinterest] { --brand: #e60023; }
[data-share=reddit] { --brand: #ff4500; }
[data-share=whatsapp] { --brand: #25d366; }
/* The email form used by the email signup variant */
.locker-form {
    display: flex;
    gap: 8px;
    width: 100%;
    max-width: 340px;
}
.locker-form input {
    flex: 1;
    min-width: 0;
    padding: 10px 14px;
    font-size: 14px;
    color: #fff;
    background-color: #1e293b;
    border: 1px solid #475569;
    border-radius: 6px;
}
.locker-form input::placeholder {
    color: #94a3b8;
}
.locker-form button {
    padding: 10px 18px;
    font-size: 14px;
    font-weight: 700;
    color: #fff;
    background-color: var(--locker-accent);
    border: 0;
    border-radius: 6px;
    cursor: pointer;
}
.locker-form button:hover {
    filter: brightness(.9);
}
/* Keep things calm for visitors who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
    .locker-content, .locker-overlay, .locker-share a {
        transition: none;
    }
}

The knobs. Every value you might want to tweak (corner radius, blur strength, animation speed, accent color, button size) is a CSS custom property defined once on .locker, and each share button reads its background from its own --brand variable. Restyling the whole locker means changing a few lines in one place.

The lock. While the locker doesn't have the unlocked class, the content is blurred, pointer-events: none stops clicks reaching the download link, and user-select: none stops drag-selecting the text through the blur.

The unlock. Adding one class does all the work: the overlay's opacity transitions to zero, visibility: hidden takes it out of the way once the fade ends, and the blur transitions away on the content underneath. The prefers-reduced-motion block swaps the animation for an instant reveal for visitors who ask for that.

4. Unlocking with JavaScript

Now the part that makes it work. Save this as locker.js and include it at the end of your body. It handles every locker on the page, the share popups, the unlock, and the memory of past unlocks:

JS
// The share dialog URL for every supported network; {url} becomes the page's address
const networks = {
    x: 'https://x.com/intent/post?url={url}',
    facebook: 'https://www.facebook.com/sharer/sharer.php?u={url}',
    linkedin: 'https://www.linkedin.com/sharing/share-offsite/?url={url}',
    pinterest: 'https://pinterest.com/pin/create/button/?url={url}',
    reddit: 'https://www.reddit.com/submit?url={url}',
    whatsapp: 'https://wa.me/?text={url}'
};
// Read or save an unlock flag; storage can be blocked in private or embedded pages
const remember = (key, value) => {
    try { return value ? localStorage.setItem(key, value) : localStorage.getItem(key); } catch (e) { return null; }
};
// Reveal the content and remember the unlock so it stays open on return visits
const unlock = locker => {
    locker.classList.add('unlocked');
    locker.querySelector('.locker-content').inert = false;
    remember('locker_' + locker.dataset.locker, 'unlocked');
};
// Set up every locker on the page
document.querySelectorAll('.locker').forEach(locker => {
    // Keep clicks and keyboard focus out of the content while it is locked
    locker.querySelector('.locker-content').inert = true;
    // Unlocked on a previous visit? Reveal it immediately and skip the rest
    if (remember('locker_' + locker.dataset.locker)) return unlock(locker);
    // Unlock by sharing: one click handler per share button
    locker.querySelectorAll('[data-share]').forEach(button => button.addEventListener('click', event => {
        event.preventDefault();
        // Build the share link for the clicked network with this page's URL filled in
        const url = networks[button.dataset.share].replace('{url}', encodeURIComponent(location.href));
        // Open the network's share dialog in a small centered popup window
        const popup = window.open(url, 'share', 'popup,width=600,height=600,left=' + (screen.width - 600) / 2 + ',top=' + (screen.height - 600) / 2);
        // Popup blocked? Fall back to a new tab and unlock after a short delay
        if (!popup) { window.open(url, '_blank'); setTimeout(() => unlock(locker), 5000); return; }
        // Watch the popup; once it closes, the share is done and we can unlock
        const watcher = setInterval(() => {
            if (popup.closed) { clearInterval(watcher); unlock(locker); }
        }, 500);
    }));
    // Unlock by email: used by the email signup variant if the form exists
    const form = locker.querySelector('.locker-form');
    if (form) form.addEventListener('submit', event => {
        event.preventDefault();
        // Send the address to your newsletter system here, then reveal the content
        unlock(locker);
    });
});

The share URLs. Every network exposes a public share endpoint, and the networks object holds a template for each with a {url} placeholder. When a button is clicked, the script swaps in the current page's address with encodeURIComponent(location.href), so the locker shares whatever page it happens to be on.

Watching the popup. The share dialog opens in a small centered popup, and a setInterval checks its closed property twice a second. Share dialogs close themselves after posting, and visitors close them by hand either way, so the window closing is our signal to unlock. If the browser blocked the popup, we open the share link in a normal tab instead and unlock a few seconds later.

Remembering the unlock. The unlock() function adds the unlocked class, lifts the inert attribute that kept keyboard focus out of the hidden content, and saves a flag keyed by the locker's data-locker value; on the next visit that flag opens the content immediately. The remember() helper wraps localStorage in a try/catch, so the locker still works where storage is blocked.

The unlocked content locker showing the revealed download card with its download button
After the share window closes, the overlay fades out and the download is clickable.

On phones you can offer the native share sheet instead: navigator.share() returns a promise that resolves once the visitor completes the share, so you can await it and call unlock() with real confirmation.

Want to poke at the finished thing before installing it? The complete locker is loaded into our code editor where you can run and tweak it live.

5. Locking Content Behind an Email Signup

Shares are nice, but an email list is an asset. The same locker handles that with one change: swap the share buttons in the overlay for a small form. The JavaScript above already looks for a locker-form element, so no new script is needed.

HTML
<!-- The overlay asks for an email address instead of a share -->
<div class="locker-overlay">
    <svg class="locker-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M22,6C22,4.89 21.1,4 20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6M20,6L12,11L4,6H20M20,18H4V8L12,13L20,8V18Z"/></svg>
    <p class="locker-title">Subscribe to unlock</p>
    <p class="locker-hint">Join the newsletter and the download unlocks instantly. No spam, ever.</p>
    <form class="locker-form">
        <input type="email" name="email" placeholder="Your email address" required>
        <button type="submit">Unlock</button>
    </form>
</div>
The email signup variant of the content locker asking for an email address to unlock
The email variant: the browser validates the address and the same script unlocks the content.

The required and type="email" attributes make the browser reject empty and malformed addresses before the submit handler ever runs. The handler currently unlocks on the spot; in a real setup you'd first send the address to your mailing list, which our newsletter system tutorial walks through building with PHP and MySQL.

6. SEO, Bypassing, and Good Practice

Let's be honest about what this is: a nudge, not a vault. The locked content is still in the page's HTML, and anyone comfortable with DevTools can read it there. For a cheat sheet or a coupon code that is fine, and it is why search engines can still index the page. For anything with real value, keep the file on your server and only hand it out after a verified signup.

Do

  • Lock bonus extras like downloads
  • Keep indexable content in the HTML
  • Remember unlocks with localStorage
  • Offer several networks to choose from
  • Serve valuable files from the server

Don't

  • Lock the article people came to read
  • Show crawlers different content than visitors
  • Make returning visitors share again
  • Guilt-trip visitors with pushy wording
  • Trust a hidden link to protect paid work

Customizing it is mostly CSS. The brand colors live in one block, the icons are swappable symbols, and because every locker is configured by its data-locker ID, you can run several on one page, each remembering its own unlock. If you want a different action entirely, watch a YouTube subscribe click, count a timer down, anything, just call unlock() when you're satisfied.

7. Frequently Asked Questions

What is a content locker?

A content locker is a widget that hides part of a page until the visitor completes an action, most commonly sharing the page or subscribing with their email address. It is a value exchange: the visitor gets a download or bonus content, and the site gets reach or a new subscriber. This tutorial builds one from scratch with plain HTML, CSS, and JavaScript.

Can visitors bypass a JavaScript content locker?

Yes. A client-side locker keeps the content in the page and hides it visually, so anyone can read it through the browser's DevTools or by disabling JavaScript. Treat it as an engagement nudge, not security. Content that genuinely needs protecting should stay on the server and only be delivered after a verified action, like a confirmed signup or a purchase.

Does a content locker hurt SEO?

Not when it is built like this one. The locked content stays in the HTML, so search engines crawl and index the page exactly as before; only human visitors see the blur and overlay. What does hurt is locking the page's primary content or serving crawlers something different from what visitors get, which is cloaking. Lock the bonus, never the article.

How does the locker know the visitor actually shared?

It doesn't, not with certainty. Social networks no longer offer public APIs that confirm a share, so the practical signal is the share window closing, which normally happens right after posting. A visitor could close it without sharing; every share locker accepts that trade-off. On mobile, navigator.share() resolves only after a completed share, a stronger signal.

Which social networks should a share locker include?

X, Facebook, LinkedIn, Pinterest, Reddit, and WhatsApp cover essentially every audience in 2026, and all six are included in this tutorial with their current share endpoints. WhatsApp is easy to overlook and converts especially well on mobile. Skip anything you remember from older tutorials like Google+ or StumbleUpon; both networks are long gone.

Can I lock content behind an email signup instead of a share?

Yes, and it is often the better trade since an email subscriber is worth more than one share. This tutorial's script supports it out of the box: replace the share buttons with a small email form in the overlay, and the same unlock logic runs when the form is submitted. Hook the form up to your mailing list on the server to actually store the address.

Conclusion

That's a complete, modern content locker in a couple hundred lines you fully control: no plugin subscriptions, no icon fonts, no tracking scripts. It blurs the goods, unlocks on a share or a signup, respects keyboards and screen readers, and doesn't pester anyone twice thanks to localStorage.

Grab the full source with the free download below and drop it into your site. If you build on it, our newsletter system pairs naturally with the email variant, and toast notifications make a nice "Unlocked!" confirmation. If this helped, a share would be fitting - you know where the buttons are.

Download the Source Code

Skip the copy-pasting and get the complete, working project from this tutorial as a free ZIP with a setup guide included. Free for personal and commercial use.

Free Download