Plugins & Extending jQuery

Writing a jQuery plugin with $.fn, chainability, handling multiple elements, and a complete character-counter example.

$.fn — where every jQuery method actually lives

Every method you've used so far (.text(), .on(), .addClass()) is just a function attached to $.fn, the prototype every jQuery object inherits from. Writing a jQuery plugin means adding your own function to that same object — the instant you do, it's callable on any jQuery selection, exactly like a built-in method:

Javascript
$.fn.highlight = function () {
    return this.css('background-color', 'yellow');
};
Javascript
$('.important').highlight();

this inside a plugin function refers to the jQuery object the plugin was called on — whatever elements $('.important') matched. Wrapping the whole thing in an IIFE (immediately-invoked function expression) that passes in jQuery as $ is the standard, safe way to define a plugin, since it guarantees $ means jQuery inside the plugin even on a page where $ has been reassigned to something else (a real risk when multiple libraries share the global $):

Javascript
(function ($) {
    $.fn.highlight = function () {
        return this.css('background-color', 'yellow');
    };
}(jQuery));

Chainability: always return this

jQuery's fluent, chainable style — $('#el').addClass('x').css('color', 'red') — only works because every built-in method returns the same jQuery object it operated on. A plugin that doesn't do the same breaks the chain for anyone using it:

Javascript
$.fn.highlight = function () {
    this.css('background-color', 'yellow'); // no return — breaks chaining
};

$('.important').highlight().fadeOut(); // TypeError: .fadeOut is not a function on undefined
Javascript
$.fn.highlight = function () {
    return this.css('background-color', 'yellow'); // returns the jQuery object — chaining works
};

$('.important').highlight().fadeOut(); // works correctly

Handling multiple matched elements with .each()

A jQuery selection can match zero, one, or many elements, and a plugin should behave correctly for all three. Wrapping the plugin body in .each() — and still returning this afterward — is the standard pattern once a plugin needs to do more than a single chainable jQuery call per element (like attaching a separate event handler to each matched element individually):

Javascript
(function ($) {
    $.fn.confirmDelete = function (message) {
        return this.each(function () {
            const $el = $(this);

            $el.on('click', function (event) {
                if (!window.confirm(message || 'Are you sure?')) {
                    event.preventDefault();
                }
            });
        });
    };
}(jQuery));
HTML
<a href="/posts/42/delete" class="delete-link">Delete post</a>
<a href="/posts/43/delete" class="delete-link">Delete post</a>
Javascript
$('.delete-link').confirmDelete('Delete this post permanently?');

this.each(function () { ... }) runs the callback once per matched element, with this inside the callback bound to the raw DOM element (hence wrapping it as $(this) to use jQuery methods on it) — and because .each() itself returns the original jQuery object, return this.each(...) satisfies chainability automatically, with no separate return this; needed afterward.

Accepting options, with sensible defaults

A configurable plugin merges caller-supplied options over a set of defaults using $.extend(), which is jQuery's shallow object-merge utility:

Javascript
(function ($) {
    $.fn.characterCounter = function (options) {
        const settings = $.extend({
            maxLength: 280,
            counterClass: 'char-counter',
        }, options);

        return this.each(function () {
            const $input = $(this);
            const $counter = $('<div>')
                .addClass(settings.counterClass)
                .insertAfter($input);

            function updateCount() {
                const remaining = settings.maxLength - $input.val().length;
                $counter.text(remaining + ' characters remaining');
                $counter.toggleClass('over-limit', remaining < 0);
            }

            $input.on('input', updateCount);
            updateCount(); // show the correct count immediately, before any typing happens
        });
    };
}(jQuery));
HTML
<textarea id="bio" placeholder="Tell us about yourself"></textarea>
Css
.char-counter { font-size: 0.85rem; color: #6b7280; margin-top: 0.25rem; }
.char-counter.over-limit { color: #dc2626; }
Javascript
$('#bio').characterCounter({ maxLength: 150 });

Any option not supplied by the caller falls back to the corresponding default — calling $('#bio').characterCounter() with no arguments at all still works correctly, using the built-in 280-character default and the default counter class name.

A complete example, put together

HTML
<textarea id="tweet" maxlength="500"></textarea>
<textarea id="comment"></textarea>
Javascript
(function ($) {
    $.fn.characterCounter = function (options) {
        const settings = $.extend({
            maxLength: 280,
            counterClass: 'char-counter',
        }, options);

        return this.each(function () {
            const $input = $(this);
            const $counter = $('<div>').addClass(settings.counterClass).insertAfter($input);

            function updateCount() {
                const remaining = settings.maxLength - $input.val().length;
                $counter.text(remaining + ' characters remaining');
                $counter.toggleClass('over-limit', remaining < 0);
            }

            $input.on('input', updateCount);
            updateCount();
        });
    };
}(jQuery));

$('#tweet').characterCounter({ maxLength: 280 });
$('#comment').characterCounter({ maxLength: 1000, counterClass: 'char-counter comment-counter' });

Calling .characterCounter() on two entirely different textareas, each with its own options, demonstrates the whole point of writing it as a proper $.fn plugin in the first place: the same reusable behavior, safely applied to as many independent elements as needed, each configured differently, with no copy-pasted setup code.

Common mistakes

  • Forgetting to return this (or return this.each(...)) from a plugin function — it works fine on its own, but silently breaks chaining the moment someone tries to call another jQuery method right after it.
  • Not wrapping the plugin in an IIFE with jQuery passed in as $ — on a page where another library has reassigned the global $ (a real possibility when several old scripts coexist), the plugin can reference the wrong thing entirely.
  • Writing plugin logic that assumes exactly one matched element, instead of using .each() — it appears to work in casual testing against a single element, then behaves incorrectly (usually only affecting the first matched element) the moment it's called against a selector matching several.
  • Mutating the caller's options object directly instead of merging it with $.extend() into a fresh settings object — this can produce confusing bugs if the same options object is reused across multiple plugin calls elsewhere in the codebase.