Introduction
Crafting engaging user experiences represents both an enjoyable and valuable pursuit in modern web development. Fundamentally, this work involves enhancing the daily interactions of countless users across digital platforms. Most user interface developers focus on the end goal—seeing their products actively used by real people. The faster we can reach that destination without sacrificing quality, the better we all fare in this profession.
The jQuery framework gained tremendous popularity among developers largely due to its "write less, do more" philosophy—a philosophy that extends directly into jQuery UI. Modern HTML and CSS standards provide powerful tools for asssembling robust, responsive user interfaces. However, when browser inconsistencies arise—along with the absence of consistent development conventions and patterns across projects—jQuery UI steps in to fill those gaps. Rather than reinventing how we build web applications, jQuery UI focuses on progressively enhancing existing browser capabilities.
Like any framework, jQuery UI does not suit every developer or every project scenario. The framework acknowledges this reality and provides extensibility mechanisms for most situations you might encounter. This book aims to share practical experience gained from working extensively with jQuery UI widgets—pushing boundaries, extending functionality, and modifying behavior when necessary. Most techniques presented here will prove valuable regardless of what type of applications you build.
What This Book Covers
The first chapter explores accordion widgets, covering drag-and-drop between accordion containers, keyboard navigation enhancements, dynamic height adjustments, and theme-based spacing modifications.
The second chapter examines autocomplete functionality, explaining how to leverage multiple data sources, transform select elements into autocomplete widgets, implement remote data filtering, and apply visual effects to dropdown menus.
The third chapter demonstrates button customization techniques, ranging from simple checklist implementations to complex buttonset handling with spacing controls, automatic width synchronization, and hover state animations.
The remaining chapters cover datepicker integration, dialog management, sortable menus, progress bar enhancements, slider modifications, spinner formatting, tab navigation techniques, tooltip applications, and advanced widget development patterns including Backbone.js integration.
What You Will Need
To work through the examples in this book, you will need a modern web browser for running demonstrations, a text editor for reviewing and modifying code samples, and all JavaScript dependencies included with the downloadable examples. Python is optional—some examples require a web server, and the built-in Python server works well for demonstration purposes. Any web server with appropriate configuration will suffice.
Who This Book Is For
This book targets jQuery UI developers who want to improve existing applications, extract ideas for new projects, or gain deeper understanding of the overall widget architecture. Readers should have basic familiarity with jQuery UI concepts and some experience writing code that uses jQuery UI components. Recipes target intermediate-level developers. Each recipe functions independently while connecting to related techniques that guide further exploration.
Chapter 1: Building Advanced Accordions
This chapter explores techniques for extending accordion widgets to handle diverse scenarios. The accordion widget provides substantial out-of-the-box functionality—a themed container component that groups content into collapsible sections without requiring any configuration. We focus on revealing the internal workings of accordion widgets through practical use cases.
Keyboard navigation represents an important page traversal mechanism that can be enhanced with accordion support. Height transitions during section expansion involve complex calculations that require careful handling when content changes dynamically. Additionally, we examine user-controlled section sizing and theme-based spacing adjustments. Advanced scenarios include allowing users to freely reorder accordion sections and drag sections between different accordion containers.
Implementing Tab-Based Section Navigation
In most desktop environments, the Tab key serves as a powerful navigation tool that many users rely upon extensively. We can leverage the tabindex attribute in modern web applications to enable Tab key navigation, telling browsers the sequential order for focus management.
However, implementing Tab navigation with accordion widgets is not as straightforward as it might initially appear. Specifying tabindex values on section headers does not produce the expected Tab key behavior. Instead, the default widget implementation provides alternative keyboard navigation using up and down arrow keys. Enhancing the widget to support familiar Tab key navigation for moving between sections, while preserving the default keyboard navigation, creates a more accessible experience.
Getting Started
First, we need a basic accordion structure with simple content in each section to visualize Tab key behavior changes during implementation. Here is the foundation markup:
<div id="accordion-widget">
<h3>First Section</h3>
<div>
<p>Content for the first section appears here.</p>
</div>
<h3>Second Section</h3>
<div>
<p>Content for the second section appears here.</p>
</div>
<h3>Third Section</h3>
<div>
<p>Content for the third section appears here.</p>
</div>
<h3>Fourth Section</h3>
<div>
<p>Content for the fourth section appears here.</p>
</div>
</div>
The following JavaScript instantiates the accordion widget with collapsible sections enabled:
(function($) {
$('#accordion-widget').accordion({
collapsible: true
});
})(jQuery);
The collapsible option allows all sections to collapse completely, which proves useful for observing focus behavior during keyboard navigation experiments. The default up and down arrow key navigation works immediately, but the Tab key produces no response in the default implementation.
Solution
We extend the accordion widget by adding a custom event handler for the keydown event. The default accordion implementation already handles up, down, left, right, and Enter keys through its keydown event processing. Rather than replacing this functionality, we add custom logic to handle Tab and Shift+Tab combinations:
(function($, undefined) {
$.widget('ui.customAccordion', $.ui.accordion, {
_create: function() {
this._super('_create');
this._on(this.headers, {
keydown: '_handleTabNavigation'
});
},
_handleTabNavigation: function(event) {
if (event.altKey || event.ctrlKey) {
return;
}
if (event.keyCode !== $.ui.keyCode.TAB) {
return;
}
var headerElements = this.headers.toArray(),
currentPosition = headerElements.indexOf(event.target),
targetElement = null;
if (event.shiftKey && currentPosition - 1 >= 0) {
targetElement = headerElements[currentPosition - 1];
}
if (!event.shiftKey && currentPosition + 1 < headerElements.length) {
targetElement = headerElements[currentPosition + 1];
}
if (targetElement) {
$(event.target).attr('tabIndex', -1);
$(targetElement).attr('tabIndex', 0);
targetElement.focus();
event.preventDefault();
}
}
});
})(jQuery);
(function($) {
$('#accordion-widget').customAccordion({
collapsible: true
});
})(jQuery);
How It Works
This implementation creates a new accordion widget by extending the default behavior. The key advantage of extension over direct modification is that all accordion instances receive the enhanced functionality without requiring separate configuration.
The _create() method replacement first invokes the original implementation using _super() to preserve all default accordion setup behavior. After this foundation is established, we bind our custom _handleTabNavigation handler to the keydown event on all accordion headers.
The navigation handler ignores events involving modifier keys (Alt or Ctrl) and immediately returns if the pressed key is not Tab. The core logic determines the target header based on Shift key presence and current position within the header collection. If movement would exceed the bounds of available headers, we allow default browser behavior to take over rather than interfering with expected functionality.
When a valid target exists, we adjust tabIndex values to maintain proper focus management—the current element receives tabIndex -1 while the target receives tabIndex 0. This ensures the target element becomes focusable and receives focus immediately.
Dynamic Height Style Management
Accordion widgets serve as containers for organizing and displaying other interface elements. Treating accordion sections as static content containers represents a common misconception. Section content frequently changes—user-triggered events may create new elements within sections, or contained components may resize dynamically. When section heights change, we must handle these transitions appropriately.
The heightStyle option controls how accordion section heights are calculated. The default behavior makes all sections equal to the tallest section's height, which works well when content sizes remain consistent. However, applications that dynamically load content into specific sections eventually reach a point where automatic height management no longer makes sense.
Getting Started
Consider this accordion structure where one section contains significantly more content than the others:
<div id="resizable-accordion">
<h3>Section A</h3>
<div>
<p>Minimal content here</p>
</div>
<h3>Section B</h3>
<div>
<ul>
<li>First item</li>
<li>Second item</li>
<li>Third item</li>
<li>Fourth item</li>
</ul>
</div>
<h3>Section C</h3>
<div>
<p>Also minimal content</p>
</div>
</div>
The following initialization uses default height behavior:
(function($) {
$('#resizable-accordion').accordion();
})(jQuery);
With the default heightStyle value of 'auto', all sections match the height of the tallest section (Section B), creating unnecessary empty space in sections with minimal content.
Solution
We extend the accordion widget to support dynamic heightStyle changes at runtime. The default implementation does not handle runtime height style changes gracefully—we address this limitation through custom refresh logic:
(function($, undefined) {
$.widget('ui.flexibleAccordion', $.ui.accordion, {
refresh: function() {
this._super('refresh');
if (this.options.heightStyle !== 'content') {
return;
}
this.headers.next().each(function() {
var contentSection = $(this);
if (contentSection.css('height')) {
contentSection.css('height', '');
}
});
}
});
})(jQuery);
(function($) {
$('#resizable-accordion').accordion();
for (var itemIndex = 0; itemIndex < 20; itemIndex++) {
$('ul').append('<li>Dynamic item ' + itemIndex + '</li>');
}
$('#resizable-accordion')
.accordion('option', 'heightStyle', 'content')
.accordion('refresh');
})(jQuery);
How It Works
This extension overrides the refresh() method to enable heightStyle changes during widget operation. When heightStyle is set to 'content', each section uses only the height necessary to display its content rather than matching the tallest section.
The modified refresh implementation clears explicit height values from content sections, allowing natural height calculation based on content. Without this extension, runtime heightStyle changes would leave orphaned height values, causing unexpected scrollbars or layout issues.
Creating Resizable Content Sections
Resizable content sections empower users to adjust section heights by dragging section boundaries. This approach provides flexibility beyond the heightStyle attribute, allowing users to customize accordion layouts according to their preferences. If a section contains excess vertical space, users can reduce its height to improve visibility of the accordion and surrounding interface elements.
Solution
We extend the accordion widget's _create() method to apply jQuery UI's resizable interaction widget to each content div:
(function($, undefined) {
$.widget('ui.resizableAccordion', $.ui.accordion, {
_create: function() {
this._super('_create');
this.headers.next()
.resizable({
handles: 's'
})
.css('overflow', 'hidden');
},
_destroy: function() {
this.headers.next()
.resizable('destroy')
.css('overflow', '');
this._super('_destroy');
}
});
})(jQuery);
(function($) {
$('#resizable-accordion').resizableAccordion();
})(jQuery);
How It Works
The custom _create() implementation first establishes the default accordion foundation, then locates all content sections and applies the resizable widget. The configuration specifies handles: 's', meaning users can only resize sections using a southern handle positioned at each section's bottom edge.
The custom _destroy() implementation cleans up resizable state before invoking the original destruction logic. This includes removing the overflow CSS property and destroying resizable widget instances.
Extension: Toggleable Resizability
We can add configuration options to control resizable behavior:
(function($, undefined) {
$.widget('ui.resizableAccordion', $.ui.accordion, {
options: {
enableResizing: true
},
_create: function() {
this._super('_create');
if (!this.options.enableResizing) {
return;
}
this.headers.next()
.resizable({
handles: 's'
})
.css('overflow', 'hidden');
},
_destroy: function() {
this._super('_destroy');
if (!this.options.enableResizing) {
return;
}
this.headers.next()
.resizable('destroy')
.css('overflow', '');
}
});
})(jQuery);
(function($) {
$('#resizable-accordion').resizableAccordion({
enableResizing: false
});
})(jQuery);
Theme-Based Spacing Control
Accordion section spacing is controlled by the CSS theming framework. The visual structure of accordions is defined by a set of CSS rules that determine spacing between sections. We can override theme-provided styles to achieve desired spacing configurations.
Solution
Create a custom stylesheet that overrides default accordion spacing:
.ui-accordion .ui-accordion-header {
margin-top: 8px;
}
.ui-accordion .ui-accordion-header:first-child {
margin-top: 0;
}
Include this stylesheet after the default jQuery UI theme in your HTML document:
<link rel="stylesheet" href="path/to/jquery-ui.css">
<link rel="stylesheet" href="theme.accordion.css">
How It Works
We duplicate the CSS selectors found in jQuery UI themes and modify specific properties affecting section spacing. The custom margin values override theme defaults. Placing the custom stylesheet after the default theme ensures our modifications take precedence.
Sorting Accordion Sections
Using the sortable interaction widget, we transform static accordion section layouts into user-reorderable configurations. Users can drag sections to preferred positions within the accordion container.
Solution
We extend the accordion widget to include sortable section functionality controlled by a configuration option:
(function($, undefined) {
$.widget('ui.sortableAccordion', $.ui.accordion, {
options: {
allowSorting: false
},
_create: function() {
this._super('_create');
if (!this.options.allowSorting) {
return;
}
var headers = this.headers;
headers.each(function() {
var headerElement = $(this);
headerElement.next()
.addBack()
.wrapAll('<div class="accordion-section"/>');
});
this.element.sortable({
axis: 'y',
handle: 'h3',
stop: function(event, ui) {
ui.item.children('h3')
.triggerHandler('focusout');
}
});
},
_destroy: function() {
if (!this.options.allowSorting) {
this._super('_destroy');
return;
}
this.element.sortable('destroy');
this.headers.each(function() {
$(this).unwrap('<div/>');
});
this._super('_destroy');
}
});
})(jQuery);
(function($) {
$('#sortable-accordion').sortableAccordion({
allowSorting: true
});
})(jQuery);
How It Works
The sortable widget requires specific DOM structure to function correctly with accordion sections. By wrapping each header-content pair in a div container, we create movable units that preserve section integrity during dragging operations.
The custom _create() implementation iterates through headers, wrapping each header with its corresponding content section. The sortable widget then operates on these wrapped containers, restricting movement to the vertical axis and limiting drag handles to header elements.
The custom _destroy() implementation reverses these modifications—unwrapping elements and destroying the sortable widget instance—before invoking the parent destruction logic.
Drag and Drop Between Accordions
Some applications require fluid layouts that adapt to various screen resolutions and functional requirements. Accordion widgets serve as static grouping components that organize related items into collapsible sections. We can extend this concept by supporting section transfer between different accordion containers.
Getting Started
We need two accordion containers for this demonstration:
<div id="source-accordion" style="width: 30%; float: left;">
<h3>Movable Section A</h3>
<div>
<p>This section can be dragged to another accordion.</p>
</div>
<h3>Movable Section B</h3>
<div>
<p>This section can also be moved elsewhere.</p>
</div>
<h3>Movable Section C</h3>
<div>
<p>Consider moving this section as well.</p>
</div>
</div>
<div style="width: 5%; float: left;"> </div>
<div id="target-accordion" style="width: 30%; float: left;">
<h3>Destination Section 1</h3>
<div>
<p>Sections dragged here will be accepted.</p>
</div>
<h3>Destination Section 2</h3>
<div>
<p>Additional content area for dropped sections.</p>
</div>
</div>
Solution
We extend the accordion widget with drag-and-drop capabilities using both draggable and sortable interaction widgets:
(function($, undefined) {
$.widget('ui.dndAccordion', $.ui.accordion, {
options: {
dragTarget: null,
dropAccept: null,
headerSelector: '> h3, > div > h3'
},
_clearEventBindings: function(eventNames) {
var widget = this;
if (!eventNames) {
return;
}
$.each(eventNames.split(' '), function(index, eventName) {
widget._off(widget.headers, eventName);
});
},
_setupDraggableSource: function() {
var widget = this,
draggableConfig = {
handle: 'h3',
helper: 'clone',
connectToSortable: this.options.dragTarget
};
this.headers.each(function() {
var header = $(this);
header.next()
.addBack()
.wrapAll('<div class="draggable-section"/>')
.parent()
.draggable(draggableConfig);
});
},
_setupDroppableTarget: function() {
var widget = this,
targetWidget = $(this.options.dropAccept).data('uiDndAccordion'),
sortableConfig = {
handle: 'h3',
placeholder: 'accordion-placeholder',
stop: function(event, ui) {
var droppedItem = $(ui.item),
droppedHeader = droppedItem.find('> h3'),
draggableMarker = 'ui-draggable',
originalId;
if (!droppedItem.hasClass(draggableMarker)) {
return;
}
originalId = droppedHeader.attr('id');
droppedHeader.attr('id', '');
widget.headers = widget.element.find(widget.options.headerSelector);
widget._clearEventBindings(widget.options.event);
widget._clearEventBindings('keydown');
widget._hoverable(droppedHeader);
widget._focusable(droppedHeader);
widget._on(widget.headers, {
keydown: '_keydown'
});
droppedItem.removeClass(draggableMarker);
$('#' + originalId).parent().fadeOut('slow', function() {
$(this).remove();
targetWidget.refresh();
});
}
};
this.headers.each(function() {
$(this).next()
.addBack()
.wrapAll('<div class="sortable-section"/>');
});
this.element.sortable(sortableConfig);
},
_create: function() {
this._super('_create');
if (this.options.dragTarget) {
this._setupDraggableSource();
}
if (this.options.dropAccept) {
this._setupDroppableTarget();
}
},
_destroy: function() {
this._super('_destroy');
if (this.options.dragTarget || this.options.dropAccept) {
this.headers.each(function() {
$(this).next()
.addBack()
.unwrap('<div/>');
});
}
}
});
})(jQuery);
(function($) {
$('#source-accordion').dndAccordion({
dragTarget: '#target-accordion'
});
$('#target-accordion').dndAccordion({
dropAccept: '#source-accordion'
});
})(jQuery);
Required stylesheet additions:
.accordion-placeholder {
border: 2px dashed #ccc;
min-height: 50px;
}
.draggable-section {
cursor: move;
}
How It Works
This implementation adds two configuration options to the accordion widget: dragTarget specifies where sections can be dragged, while dropAccept specifies which accordion sections can be received. Together, these options establish a drag-and-drop contract between two accordion instances.
The source accordion (dragTarget) uses draggable widget functionality. Each section is wrapped in a container and made draggable with the clone helper option. This creates a copy for dragging while keeping the original in place until dropped.
The target accordion (dropAccept) uses sortable widget functionality. The connectToSortable option links draggable sections to this sortable accordion. When a section is dropped, the sortable widget's stop callback handles integration.
The drop handler performs several critical operations: it adjusts IDs to avoid duplicates, rebuilds the header collection to include the new section, clears and reestablishes event bindings for the new section, and removes the original section from the source accordion. The refresh() call on the target accordion ensures proper height calculations after content changes.
Chapter 2: Implementing Autocomplete Features
The autocomplete widget enhances standard HTML input elements by suggesting possible values as users type. When users need to select from a predefined set of values—such as product names, usernames, or categories—the autocomplete widget improves usability by presenting suggestions and reducing typing effort.
Styling Default Input Elements
The default autocomplete implementation preserves the visual appearance of the underlying input element. While functional requirements are met, visual enhancements through the widget and theme frameworks can improve the user experience.
Getting Started
We begin with a basic label and input structure:
<div class="input-wrapper">
<label for="autocomplete-input">Search Items: </label>
<input id="autocomplete-input"/>
</div>
Solution
We extend the autocomplete widget to apply theme framework CSS classes:
(function($, undefined) {
$.widget('ui.styledAutocomplete', $.ui.autocomplete, {
inputClasses: 'ui-widget ui-widget-content ui-corner-all',
_create: function() {
this._super('_create');
this._focusable(this.element);
this.element.addClass(this.inputClasses);
},
_destroy: function() {
this._super('_destroy');
this.element.removeClass(this.inputClasses);
}
});
})(jQuery);
(function($) {
var suggestionItems = [
'Alpha Item',
'Beta Item',
'Gamma Item',
'Delta Item'
];
$('#autocomplete-input').styledAutocomplete({
source: suggestionItems
});
})(jQuery);
Required CSS enhancements:
input.ui-autocomplete-input {
padding: 4px;
transition: border-color 0.2s;
}
input.ui-autocomplete-input:focus {
outline: none;
border-color: #4a90d9;
}
How It Works
The inputClasses property defines theme framework classes applied to the input element: ui-widget provides base theming, ui-widget-content establishes the border treatment, and ui-corner-all applies rounded corners.
The custom _create() implementation calls the parent method first, then makes the input focusable using the widget factory's _focusable() utility. This utility applies the ui-state-focus class on focus and removes it on blur, with automatic cleanup on widget destruction.
The custom CSS adds visual padding and focuses state styling while removing the default browser outline, which can appear inconsistent across browsers.
Building Data Sources from Select Elements
Using array data sources is common for autocomplete implementations, but when a select element already exists in the user interface, transforming its options into the autocomplete data source avoids redundancy.
Getting Started
We use a select element as the data source foundation:
<div class="select-wrapper">
<label for="select-autocomplete">Available Items: </label>
<select id="select-autocomplete">
<option>First Option</option>
<option>Second Option</option>
<option>Third Option</option>
<option>Fourth Option</option>
</select>
</div>
Solution
We extend the autocomplete widget to handle select elements:
(function($, undefined) {
$.widget('ui.selectAutocomplete', $.ui.autocomplete, {
inputClasses: 'ui-widget ui-widget-content ui-corner-all',
_create: function() {
if (this.element.is('select')) {
var widget = this;
this.originalElement = this.element.hide();
this.inputElement = $('<input type="text"/>')
.insertAfter(this.originalElement);
this.options.source = function(request, response) {
var filter = $.ui.autocomplete.filter,
options = widget.originalElement.find('option'),
optionValues = options.map(function() {
return $(this).val();
}).get();
response(filter(optionValues, request.term));
};
}
this._super('_create');
},
_destroy: function() {
this._super('_destroy');
this.inputElement.remove();
this.originalElement.show();
}
});
})(jQuery);
(function($) {
$('#select-autocomplete').selectAutocomplete();
})(jQuery);
How It Works
For select elements, the custom _create() implementation hides the original select and stores a reference for later restoration. A new input element replaces the select, positioned immediately after the original. The source option is reassigned to a function that extracts values from select options and applies filtering.
The map() utility converts option elements to an array of values, and the filter() function applies autocomplete matching logic. When the widget is destroyed, the original select element is restored and the temporary input element is removed.
Using Multiple Data Sources
Some autocomplete scenarios require combining multiple data sources. For example, a video selection interface might need to query both DVD and Blu-ray databases, providing users with comprehensive results from multiple catalogs.
Solution
We extend the autocomplete widget with a sources option that accepts multiple arrays:
(function($, undefined) {
$.widget('ui.multiSourceAutocomplete', $.ui.autocomplete, {
options: {
dataSources: []
},
_create: function() {
var sources = this.options.dataSources;
if (sources.length > 0) {
this.options.source = function(request, response) {
var combinedResults = [],
filter = $.ui.autocomplete.filter;
$.each(sources, function(index, sourceArray) {
$.merge(combinedResults, sourceArray);
});
response(filter(combinedResults, request.term));
};
}
this._super('_create');
}
});
})(jQuery);
(function($) {
var dvdCollection = [
'DVD Title One',
'DVD Title Two',
'DVD Title Three'
];
var blurayCollection = [
'Blu-ray Title One',
'Blu-ray Title Two',
'Blu-ray Title Three'
];
$('#multi-source-input').multiSourceAutocomplete({
dataSources: [dvdCollection, blurayCollection]
});
})(jQuery);
How It Works
The custom widget adds a dataSources option accepting an array of data arrays. The extended _create() implementation checks if multiple sources are provided, then creates a merged array using jQuery's $.merge() function. The standard filter function is applied to the combined results.
This approach scales to any number of data sources without requiring changes to the implementation logic.
Remote Autocomplete Filtering
For large datasets containing thousands of items, server-side filtering provides better performance than client-side array searching. Remote filtering eliminates the need to download entire datasets to the browser.
Solution
We implement autocomplete using the GitHub API as the data source:
(function($) {
$('#github-autocomplete').autocomplete({
minLength: 3,
source: function(request, response) {
$.ajax({
url: 'https://api.github.com/legacy/repos/search/' + request.term,
dataType: 'jsonp',
success: function(apiResponse) {
var repositories = apiResponse.data.repositories.slice(0, 10);
var mappedItems = $.map(repositories, function(repo) {
return {
label: repo.name + ' (' + repo.language + ')',
value: repo.name
};
});
response(mappedItems);
},
error: function() {
response([]);
}
});
}
});
})(jQuery);
How It Works
The minLength option ensures API queries only execute when at least three characters are entered, reducing unnecessary server requests. The source function performs an AJAX request to the GitHub API, specifying JSONP for cross-domain compatibility.
The success callback maps repository data to autocomplete-compatible objects containing label and value properties. The label property displays the repository name with its primary programming language, while the value property provides the base name for selection.
Enhanced Implementation with Request Caching
We can reduce network traffic by caching remote filter results locally:
(function($, undefined) {
$.widget('ui.cachedAutocomplete', $.ui.autocomplete, {
requestCache: {},
_search: function(searchTerm) {
var responseCallback = this._response(),
cache = this.requestCache;
this.pending++;
this.element.addClass('ui-autocomplete-loading');
this.cancelSearch = false;
if (searchTerm in cache) {
responseCallback(cache[searchTerm]);
} else {
this.source({
term: searchTerm
}, responseCallback);
}
}
});
})(jQuery);
(function($) {
$('#cached-autocomplete').cachedAutocomplete({
minLength: 3,
source: function(request, response) {
var widget = this;
$.ajax({
url: 'https://api.github.com/legacy/repos/search/' + request.term,
dataType: 'jsonp',
success: function(apiResponse) {
var repositories = apiResponse.data.repositories.slice(0, 10);
var mappedItems = $.map(repositories, function(repo) {
return {
label: repo.name + ' (' + repo.language + ')',
value: repo.name
};
});
widget.requestCache[request.term] = mappedItems;
response(mappedItems);
}
});
}
});
})(jQuery);
The extended widget adds a requestCache property. When _search() is called, it checks the cache before initiating network requests. Successful responses are stored in the cache for subsequent identical queries.
Custom Rendering with Categories
Enhancing autocomplete dropdowns with category information helps users quickly identify the context of suggested items. We extend the autocomplete widget to display categories alongside suggestions.
Solution
(function($, undefined) {
$.widget('ui.categorizedAutocomplete', $.ui.autocomplete, {
_renderMenu: function(menuElement, items) {
var widget = this,
currentCategory = '';
items.sort(function(itemA, itemB) {
return itemA.category > itemB.category ? 1 : -1;
});
$.each(items, function(index, item) {
if (item.category !== currentCategory) {
widget._renderCategory(menuElement, item);
currentCategory = item.category;
}
widget._renderItemData(menuElement, item);
});
},
_renderCategory: function(menuElement, item) {
return $('<li>')
.addClass('ui-autocomplete-category')
.text(item.category)
.appendTo(menuElement);
},
_renderItem: function(menuElement, item) {
return $('<li>')
.addClass('ui-autocomplete-item')
.append($('<a>')
.append($('<span>').text(item.label))
.append($('<span>').addClass('item-description').text(item.description)))
.appendTo(menuElement);
}
});
})(jQuery);
(function($) {
var categorizedItems = [
{
value: 'completed-task',
label: 'Completed Task',
description: 'A finished work item',
category: 'Completed'
},
{
value: 'inprogress-task',
label: 'In Progress Task',
description: 'An active work item',
category: 'In Progress'
},
{
value: 'another-completed',
label: 'Another Completed Item',
description: 'Additional finished work',
category: 'Completed'
}
];
$('#categorized-input').categorizedAutocomplete({
source: categorizedItems
});
})(jQuery);
Required stylesheet additions:
.ui-autocomplete-category {
font-weight: bold;
padding: 0.3em 0.5em;
margin: 0.8em 0 0.2em;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.ui-autocomplete-item > a {
display: block;
padding: 0.3em 0.5em;
}
.ui-autocomplete-item .item-description {
display: block;
font-size: 0.85em;
color: #666;
margin-top: 0.2em;
}
How It Works
The custom _renderMenu() implementation sorts items by category before rendering. It tracks the current category and calls _renderCategory() when category boundaries change. Items are rendered using the overridden _renderItem() method.
The category rendering creates a styled list item with category text. Item rendering creates a list item containing an anchor with two spans—one for the primary label and another for the description text.
Extended Filtering
We can extend autocomplete to filter across category and description fields:
(function($) {
$.ui.autocomplete.filter = function(itemArray, searchTerm) {
var matcher = new RegExp($.ui.autocomplete.escapeRegex(searchTerm), 'i');
return $.grep(itemArray, function(item) {
return matcher.test(item.category) ||
matcher.test(item.description) ||
matcher.test(item.label);
});
};
})(jQuery);
This modification replaces the standard autocomplete filter function with one that tests category, description, and label fields against the search term.
Applying Effects to Dropdown Menus
Default autocomplete dropdowns appear instantly without transition effects. Adding subtle animations enhances the visual polish of autocomplete widgets.
Solution
(function($, undefined) {
$.widget('ui.animatedAutocomplete', $.ui.autocomplete, {
_suggest: function(matchingItems) {
this._clearMenuContainer();
this._renderMenu(this.menu.element, matchingItems);
this.menu.refresh();
this._adjustMenuDimensions();
this._positionMenuContainer();
},
_clearMenuContainer: function() {
this.menu.element
.empty()
.zIndex(this.element.zIndex() + 1);
},
_positionMenuContainer: function() {
var positionConfig = $.extend({
of: this.element
}, this.options.position);
this.menu.element.position(positionConfig);
},
_adjustMenuDimensions: function() {
var menu = this.menu,
exclusionTotal = 0,
targetWidth = Math.max(
menu.element.width('').outerWidth() + 1,
this.element.outerWidth()
),
exclusionProperties = [
'borderLeftWidth',
'borderRightWidth',
'paddingLeft',
'paddingRight'
];
if (menu.element.is(':hidden')) {
menu.element.css({
display: 'block',
opacity: 0
});
}
$.each(exclusionProperties, function(index, prop) {
exclusionTotal += parseFloat(menu.element.css(prop));
});
if (menu.element.css('opacity') == 0) {
menu.element.animate({
width: targetWidth - exclusionTotal,
opacity: 1
}, {
duration: 250,
easing: 'swing'
});
} else {
menu.element.width(targetWidth - exclusionTotal);
}
},
_close: function(event) {
var menu = this.menu;
if (menu.element.is(':visible')) {
menu.element.fadeOut(150);
menu.blur();
this.isNewMenu = true;
this._trigger('close', event);
}
}
});
})(jQuery);
(function($) {
var dataItems = [
'Animated Option One',
'Animated Option Two',
'Animated Option Three',
'Animated Option Four'
];
$('#animated-input').animatedAutocomplete({
source: dataItems
});
})(jQuery);
How It Works
The extended widget overrides several internal methods to inject animation logic. The _suggest() method coordinates menu rendering and display—animation occurs during dimension adjustment.
The _adjustMenuDimensions() method calculates the target width by determining the larger of the current menu width or the input element width. Excluded properties (borders and padding) are subtracted from the target width.
When the menu is hidden, it is displayed with zero opacity before animating both width and opacity to their target values. The _close() method replaces the default hide behavior with a fadeOut animation.
Chapter 3: Creating Custom Buttons
The button widget provides a straightforward mechanism for styling HTML buttons and anchor elements using the jQuery UI theming framework. Two button types exist: individual buttons (the more common use case) and buttonsets (for styling groups of checkboxes and radio buttons in forms).
Building Simple Checklists
Creating checklists in pure HTML requires checkbox inputs with associated labels. The button widget enhances these elements with toggle functionality and state-aware styling.
Getting Started
We create a basic checkbox structure:
<div class="checkbox-container">
<input type="checkbox" id="checkbox-first"/>
<label for="checkbox-first">Option One</label>
<input type="checkbox" id="checkbox-second"/>
<label for="checkbox-second">Option Two</label>
<input type="checkbox" id="checkbox-third"/>
<label for="checkbox-third">Option Three</label>
<input type="checkbox" id="checkbox-fourth"/>
<label for="checkbox-fourth">Option Four</label>
</div>
Solution
We transform checkboxes into toggle buttons with state-aware icons:
(function($) {
$('.checkbox-container input').button({
icons: {
primary: 'ui-icon-bullet'
}
});
$('.checkbox-container input').on('change', function(event) {
var checkbox = $(this);
if (checkbox.is(':checked')) {
checkbox.button('option', {
icons: {
primary: 'ui-icon-check'
}
});
} else {
checkbox.button('option', {
icons: {
primary: 'ui-icon-bullet'
}
});
}
});
})(jQuery);
How It Works
The button widget recognizes checkbox inputs and converts them into toggle buttons. The icons.primary option specifies the default icon (bullet) shown when unchecked. The change event handler updates the icon based on checkbox state—check icon for checked state, bullet icon for unchecked state.
The button widget automatically associates labels with their corresponding inputs through the for attribute, creating a cohesive toggle interface.
Controlling Buttonset Spacing
The buttonset widget groups multiple buttons into a unified interface component. Default buttonset rendering places buttons adjacent to each other without spacing, which may not suit all design requirements.
Getting Started
We create a radio button group for the buttonset:
<div class="radio-group">
<input type="radio" id="radio-first" name="selection-group"/>
<label for="radio-first">First Choice</label>
<input type="radio" id="radio-second" name="selection-group"/>
<label for="radio-second">Second Choice</label>
<input type="radio" id="radio-third" name="selection-group"/>
<label for="radio-third">Third Choice</label>
<input type="radio" id="radio-fourth" name="selection-group"/>
<label for="radio-fourth">Fourth Choice</label>
</div>
Solution
We extend the buttonset widget with an exploded option that adds spacing between buttons:
(function($, undefined) {
$.widget('ui.explodedButtonset', $.ui.buttonset, {
options: {
exploded: false
},
refresh: function() {
this._super('refresh');
if (!this.options.exploded) {
return;
}
var buttonElements = this.buttons.map(function() {
return $(this).button('widget')[0];
});
this.element.addClass('ui-buttonset-exploded');
buttonElements.forEach(function(button) {
$(button)
.removeClass('ui-corner-left ui-corner-right')
.addClass('ui-corner-all');
});
}
});
})(jQuery);
(function($) {
$('.radio-group').explodedButtonset({
exploded: true
});
})(jQuery);
Required stylesheet additions:
.ui-buttonset-exploded .ui-button {
margin: 2px;
}
How It Works
The custom buttonset extension adds an exploded option controlling spacing between buttons. When enabled, the extended refresh() method adds spacing through the ui-buttonset-exploded class and modifies corner styling—removing shared corner classes and applying all-corner rounding to each button individually.
Automatic Width Synchronization
Button widths are determined by their content (icons and text), potentially creating inconsistent widths within button groups. Synchronizing button widths based on the widest button creates visual consistency.
Getting Started
We create buttons with varying text content:
<div class="button-row">
<button>Short</button>
<button>Medium Length Text</button>
<button>A Much Longer Button Label</button>
</div>
Solution
We extend the button widget with a width synchronization option:
(function($, undefined) {
$.widget('ui.alignedButton', $.ui.button, {
options: {
syncWidth: false
},
_create: function() {
this._super('create');
if (!this.options.syncWidth) {
return;
}
this.element.siblings(':ui-button')
.addBack()
.button('refresh');
},
refresh: function() {
this._super('refresh');
if (!this.options.syncWidth) {
return;
}
var siblingButtons = this.element
.siblings(':ui-button')
.addBack()
.children('.ui-button-text');
var widthValues = siblingButtons.map(function() {
return $(this).width();
}).get();
var maxWidth = Math.max.apply(Math, widthValues);
var currentText = this.element.children('.ui-button-text');
if (currentText.width() < maxWidth) {
currentText.width(maxWidth);
}
}
});
})(jQuery);
(function($) {
$('.button-row button').alignedButton({
syncWidth: true
});
})(jQuery);
How It Works
The custom button widget adds a syncWidth option. When enabled, the _create() implementation triggers refresh on all sibling buttons to establish initial width synchronization.
The extended refresh() method calculates width values for all button text spans, determines the maximum width, and adjusts the current button's text span to match if necessary. This creates consistent button widths across the button group.
Enhanced with Text Alignment
We can extend the implementation to adjust text alignment when width changes occur:
(function($, undefined) {
$.widget('ui.alignedButton', $.ui.button, {
options: {
syncWidth: false
},
_create: function() {
this._super('create');
if (!this.options.syncWidth) {
return;
}
this.element.siblings(':ui-button')
.addBack()
.button('refresh');
},
_destroy: function() {
this._super();
this.element.css('text-align', '');
},
refresh: function() {
this._super('refresh');
if (!this.options.syncWidth) {
return;
}
var siblingButtons = this.element
.siblings(':ui-button')
.addBack()
.children('.ui-button-text');
var widthValues = siblingButtons.map(function() {
return $(this).width();
}).get();
var maxWidth = Math.max.apply(Math, widthValues);
var currentText = this.element.children('.ui-button-text');
if (currentText.width() < maxWidth) {
currentText.width(maxWidth);
this.element.css('text-align', 'left');
}
}
});
})(jQuery);
The enhanced implementation adds a _destroy() method that clears the text-align property and sets left alignment when width synchronization occurs.
Sorting Buttons Within Groups
Using the sortable interaction widget, we enable users to reorder buttons within container elements.
Getting Started
We create a list-based button structure:
<ul class="sortable-buttons">
<li><button>Action One</button></li>
<li><button>Action Two</button></li>
<li><button>Action Three</button></li>
</ul>
Solution
(function($) {
$('.sortable-buttons a').button();
$('.sortable-buttons').sortable({
opacity: 0.7,
placeholder: 'button-placeholder'
});
})(jQuery);
Required stylesheet additions:
.sortable-buttons {
list-style: none;
padding: 0;
margin: 0;
width: 200px;
}
.sortable-buttons li {
margin: 3px 0;
}
.button-placeholder {
border: 2px dashed #aaa;
height: 30px;
}
How It Works
Buttons are created from anchor elements within list items. The sortable widget operates on the list container, making individual list items sortable. The opacity option reduces visual weight of dragged items, and the placeholder option defines the drop zone appearance.
Applying Hover State Effects
The default button widget applies hover states through simple class additions. Adding animation to hover state transitions creates a more polished interaction experience.
Solution
(function($, undefined) {
$.widget('ui.animatedButton', $.ui.button, {
options: {
animateHover: false
},
_create: function() {
this._super('create');
if (!this.options.animateHover) {
return;
}
this._off(this.element, 'mouseenter mouseleave');
this._on({
mouseenter: '_onMouseEnter',
mouseleave: '_onMouseLeave'
});
},
_onMouseEnter: function(event) {
this.element
.stop(true, true)
.addClass('ui-state-hover', 180);
},
_onMouseLeave: function(event) {
this.element
.stop(true, true)
.removeClass('ui-state-hover', 80);
}
});
})(jQuery);
(function($) {
$('div.button-examples button').animatedButton({
animateHover: true
});
})(jQuery);
How It Works
The extended button widget adds an animateHover option. When enabled, the _create() implementation removes default mouse event bindings and replaces them with custom handlers using _on().
The custom handlers use jQuery UI's animated addClass() and removeClass() methods, specifying durations (180ms for adding hover state, 80ms for removing it). The stop(true, true) call prevents animation queuing and ensures smooth transitions.
Toggleable Button Icons
Buttons can display icons only, text only, or both. However, toggling icons requires reapplying the entire icon specification rather than simply showing or hiding the existing icon. We can implement icon toggling functionality similar to the text option.
Getting Started
We create icon buttons with controls for toggling visibility:
<div class="media-controls">
<button class="play-button">Play</button>
<button class="pause-button">Pause</button>
<button class="stop-button">Stop</button>
</div>
<div class="control-links">
<a href="#" class="hide-icons">Remove Icons</a>
<a href="#" class="show-icons">Show Icons</a>
</div>
Solution
(function($, undefined) {
$.widget('ui.toggleIconButton', $.ui.button, {
options: {
showIcon: true
},
hiddenIcons: {},
_setOption: function(key, value) {
if (key !== 'showIcon') {
this._superApply(arguments);
return;
}
if (!value && !$.isEmptyObject(this.options.icons)) {
this.hiddenIcons = $.extend({}, this.options.icons);
this._super('text', true);
this._super('icons', {});
} else if (value && !$.isEmptyObject(this.hiddenIcons)) {
this._super('icons', this.hiddenIcons);
}
},
_create: function() {
if (!this.options.showIcon) {
this.hiddenIcons = $.extend({}, this.options.icons);
this.options.icons = {};
}
this._superApply(arguments);
}
});
})(jQuery);
(function($) {
$('.hide-icons').on('click', function(event) {
event.preventDefault();
$('.media-controls button').toggleIconButton('option', 'showIcon', false);
});
$('.show-icons').on('click', function(event) {
event.preventDefault();
$('.media-controls button').toggleIconButton('option', 'showIcon', true);
});
$('.media-controls button').toggleIconButton({
text: false
});
$('.play-button').toggleIconButton('option', {
icons: {
primary: 'ui-icon-play'
}
});
$('.pause-button').toggleIconButton('option', {
icons: {
primary: 'ui-icon-pause'
}
});
$('.stop-button').toggleIconButton('option', {
icons: {
primary: 'ui-icon-stop'
}
});
})(jQuery);
How It Works
The custom widget adds a showIcon option and a hiddenIcons property for storing icon configurations. The _setOption() method handles icon visibility changes—storing current icons in hiddenIcons, showing text, and clearing icons when hiding; restoring from hiddenIcons when showing.
The _create() implementation stores and clears icons during initial widget creation based on the showIcon option. The interface links trigger icon visibility changes by setting the option on all buttons simultaneously.