What a plugin is
A jQuery plugin is just a function added to $.fn. $.fn is the shared prototype of every jQuery object, so once you add a function there, every selection has it, exactly like .hide() or .addClass():
<p>One</p><p>Two</p>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
$.fn.shout = function () {
return this.each(function () {
$(this).text($(this).text().toUpperCase() + "!");
});
};
$("p").shout().css("color", "crimson");
console.log($("p").text());
</script>ONE!TWO!Two rules make it feel like a real jQuery method:
- Inside the plugin,
thisis the jQuery object the method was called on, which may hold many elements, so loop withthis.each(). - Return
this(whichthis.each()does for you), so chaining continues:.shout().css(...).
A real plugin, step by step
Let's build a character counter for text fields: $("textarea").charCounter({ max: 140 }).
<textarea id="tweet" data-max="20" rows="2" cols="40"></textarea>
<textarea id="bio" rows="2" cols="40"></textarea>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
(function ($) {
"use strict";
$.fn.charCounter = function (options) {
if (options === "destroy") {
return this.each(function () {
const $input = $(this);
$input.off(".charCounter");
$input.data("charCounter")?.$label.remove();
$input.removeData("charCounter");
});
}
return this.each(function () {
const $input = $(this);
if ($input.data("charCounter")) return; // already set up
// Defaults, then options passed in, then this element's own data- attributes
const settings = $.extend({}, $.fn.charCounter.defaults, options, $input.data());
const $label = $("<small>", { "class": settings.className, "aria-live": "polite" }).insertAfter($input);
function update() {
const left = settings.max - $input.val().length;
$label.text(left + " characters left").toggleClass("over", left < 0);
$input.trigger("charcounter:update", [left]);
}
$input.on("input.charCounter", update);
$input.data("charCounter", { settings, $label });
update();
});
};
// Public defaults, so a site can change them once for every counter
$.fn.charCounter.defaults = { max: 100, className: "char-counter" };
})(jQuery);
$("textarea").charCounter({ max: 50 });
$("textarea").on("charcounter:update", function (event, left) {
console.log(this.id, "->", left, "left");
});
</script>tweet -> 19 left
tweet -> 18 left
tweet -> 17 left
tweet -> 16 left
tweet -> 15 left
bio -> 49 left
bio -> 48 leftThe two counters use different limits: #tweet has data-max="20" in its HTML, which overrides the max: 50 passed in, while #bio uses 50. Data attributes come last in the $.extend call, so the most specific setting, the one on the element itself, wins.
Let's look at each part.
The wrapper
(function ($) {
// plugin code
})(jQuery);This function runs immediately and receives jQuery as a parameter named $. On sites where $ belongs to another library or has been released with noConflict (as in WordPress), the plugin still works. It also keeps your helper variables private.
Options and defaults
$.extend({}, $.fn.charCounter.defaults, options, $input.data()) builds the settings: defaults first, then what the caller passed, then anything set in the element's data- attributes. Exposing the defaults as $.fn.charCounter.defaults lets a site change them once for every counter:
$.fn.charCounter.defaults.max = 280;State and the double-call guard
The plugin stores its state with .data("charCounter", ...). That serves two purposes: the destroy action can find the label to remove, and calling .charCounter() twice on the same element does nothing the second time, instead of adding a second label and a second set of handlers.
Namespaced events
The handler is attached as input.charCounter. The destroy action calls .off(".charCounter"), which removes only the plugin's own handlers and leaves any the page added.
Announcing changes
$input.trigger("charcounter:update", [left]) fires a custom event, so the page can react (for example, disable a submit button) without the plugin knowing anything about it.
Calling a method on an existing plugin
The common pattern for "methods" is to pass a string, as destroy does above:
<textarea id="t"></textarea>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
$.fn.charCounter = function (options) {
if (options === "destroy") {
return this.each(function () {
$(this).off(".charCounter").data("charCounter")?.$label.remove();
$(this).removeData("charCounter");
});
}
return this.each(function () {
if ($(this).data("charCounter")) return;
const $label = $("<small>").insertAfter(this);
$(this).on("input.charCounter", () => $label.text($(this).val().length)).data("charCounter", { $label });
});
};
$("#t").charCounter().charCounter(); // second call is ignored
console.log("labels after two calls:", $("small").length);
$("#t").charCounter("destroy");
console.log("labels after destroy:", $("small").length, "| data left:", $("#t").data("charCounter"));
</script>labels after two calls: 1
labels after destroy: 0 | data left: undefinedA destroy method matters in single-page interfaces and dashboards, where parts of the page are replaced without a reload: removing the plugin cleanly prevents leaked handlers and duplicate behaviour.
Utility plugins
A plugin can also add a function to $ itself instead of $.fn, for tools that are not about elements:
$.formatPrice = function (amount, currency = "INR") {
return new Intl.NumberFormat("en-IN", { style: "currency", currency }).format(amount);
};Keep these few. A plain JavaScript module or function is usually just as good and does not depend on jQuery.
Using other people's plugins
Thousands of jQuery plugins exist: date pickers, sliders, carousels, validation. Before adding one, check that it is maintained, works with jQuery 4 (old plugins often call removed functions such as $.isArray), is accessible with a keyboard, and is worth its download size. Load plugins after jQuery and before the code that uses them.
Common mistakes
- Forgetting
return, which breaks chaining. - Working on
this[0]only, so the plugin ignores all but the first element. - Changing the shared defaults with
$.extend(defaults, options)instead of$.extend({}, defaults, options). - No guard against double initialisation.
- Un-namespaced events that the plugin cannot remove safely.
Exercise
Write a plugin $.fn.maxHeight that sets every selected element to the height of the tallest one, and returns the selection for chaining. Test it on three boxes of different heights.
Show one possible answer
<div class="col" style="height: 40px">A</div>
<div class="col" style="height: 90px">B</div>
<div class="col" style="height: 60px">C</div>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<script>
(function ($) {
$.fn.maxHeight = function () {
const tallest = Math.max(...this.map((i, el) => $(el).height()).get());
return this.height(tallest);
};
})(jQuery);
$(".col").maxHeight().css("background", "#e0f2fe");
console.log($(".col").map((i, el) => $(el).height()).get());
</script>[90, 90, 90]Key points
- A plugin is a function added to $.fn, so every jQuery object gets it: $("input").charCounter().
- Loop with this.each() and return the result, so the plugin works on many elements and stays chainable.
- Merge options with public defaults using $.extend({}, defaults, options).
- Store state with .data(), guard against running twice, namespace events, and provide a destroy action.