Collapsible UI: Robust, Animated and Accessible
There are many ways to implement a collapsible UI, but many solutions fall short as requirements increase. For example:
- A CSS-only solution will be less accessible, as it can’t update ARIA attributes, and may conflate form field controls to cleverly toggle the collapsible state
- Many solutions animate the collapsible in CSS with arbitrary dimensions, with the drawback of weird or inconsistent animation, and put limitations on the composition of the content
- Brittleness when expanding and collapsing, making it possible to break the UI when toggling repeatedly mid-transition
- Solving for brittleness by suppressing input for toggling the collapsible mid-transition, resulting in lower responsiveness and friction for the user
- Implementation that limits or is unable to support programmatic toggling, or composition of the UI’s behavior with other interactions and events
- Implementations that leaving behind inline styles that disrupt the responsive layout of the page or limit capabilities for asynchronous loading and dynamic content
There are always special situations in UI that may require more opinionated features, such as having or not having the collapsed content in the DOM when collapsed (for SEO, in-page search, or use with assistive technology), or showing a “partially collapsed” display of content. But generally, a robust collapsible UI needs to be:
- Flexible with the composition of the title, toggle icon, and content
- Compatible with other constraints and capabilities, such as asynchronously or dynamically sizing the collapsed content
- Accessible with respect to stateful ARIA attributes, keyboard navigation, tab focus, and content visibility
- Interoperable with other activities, such as being initially open or closed, toggling programmatically, or firing callbacks on interaction
The solutions in the above Codepen samples using plain JavaScript and React satisfy these qualities. As a brief description of their solution, they both:
- Markup and update ARIA attributes for expanded, controls, haspopup, and hidden
- Animate open and close using CSS transitions on height timed by JavaScript, cleaning up inline styles and timers
- Accept callbacks (React) or emit custom events (plain) for opening and closing the collapsible
- Can be composed of any kind of content design or quality, including dynamically loading or removing of content without defect to the layout or interaction; for example if a video link were to be replaced with an inline video in the collapsible, it would not be cutoff, or if the container size were adjusted, negative space in the layout would be conserved
Client-side Asset Prefetch
A simple utility function to prefetch resources associated with Webpack entries of the current site build, using a service call that responds with a set of JavaScript and CSS URLs. Helpful along the critical path of a site experience, prefetching the resources for the next predictably navigated page ahead of time can improve performance for the end user.
import { baseAPIRequest } from 'utilities/http';
function prefetchWebpackAssets (bundleNames) {
/*
Safari doesn't support link prefetch out of the box
as of 10/14/2020 https://caniuse.com/link-rel-prefetch
Safari will however, use cache from a simple fetch
Edge supports prefetch but will not use the resource from cache
if the server responds with 404 on the resource. Edge sends a
request to the server (ETag cache mechanism)
*/
const useLinkPrefetch = !/Safari/.test(navigator.userAgent);
const data = bundleNames;
const url = '/api/webpackAssets/';
const params = {
method: 'POST'
};
const request = baseAPIRequest({
url,
data,
params
});
request.then((webpackRecords) => {
if (webpackRecords.length > 0) {
const head = document.getElementsByTagName('head')[0];
webpackRecords.forEach(({ JS, CSS }) => {
if (JS) {
if (useLinkPrefetch) {
const link = document.createElement('link');
link.setAttribute('rel', 'prefetch');
link.setAttribute('as', 'script');
link.setAttribute('href', JS);
head.append(link);
} else {
fetch(JS);
}
}
if (CSS) {
if (useLinkPrefetch) {
const link = document.createElement('link');
link.setAttribute('rel', 'prefetch');
link.setAttribute('as', 'style');
link.setAttribute('href', CSS);
head.append(link);
} else {
fetch(CSS);
}
}
});
}
});
return request;
}
export default prefetchWebpackAssets;
withJSXTemplate for Backbone.js + React
A Backbone.js to React Migration Helper. This function allows a Backbone View to render its template using a React component, aiding in an incremental transition from Backbone to React as the view engine for a large application. From there, incremental improvement can be made from the leaf nodes of the view tree up, with React components replacing the controller-like responsibilities of the Backbone views.
import React from 'react';
import ReactDOM from 'react-dom';
function withJSXTemplate (View) {
const view = View.extend({
constructor (...rest) {
if (this.JSXTemplate) {
this.template = false;
}
View.call(this, ...rest);
},
render (...rest) {
if (this.JSXTemplate) {
// unmount before prototype render which could lose references or change this.el
ReactDOM.unmountComponentAtNode(this.el);
const Component = this.JSXTemplate;
const modelAttributes = this.model?.attributes || {};
const helpers = (this.templateHelpers && this.templateHelpers()) || {};
ReactDOM.render(<Component modelAttributes={modelAttributes} helpers={helpers} />, this.el);
}
View.prototype.render.call(this, ...rest);
return this;
},
destroy (...rest) {
if (this.JSXTemplate) {
ReactDOM.unmountComponentAtNode(this.el);
}
View.prototype.destroy.call(this, ...rest);
}
});
return view;
}
export default withJSXTemplate;
PlayStation 2, 7 Stars in JavaScript
This is an amusing graphical effect, originally written in C++ with OpenGL circa 2005, and later ported into JavaScript/WebGL using three.js in 2017.
Read my article about it, see it in action here, or take a look at the source: