skeletor
Skeletor was the front-end framework we built at Delphic Digital (Philadelphia, 2013–2017). It started as a Gulp + Sass boilerplate and grew through three distinct experimentation eras, pre-component utilities, a jQuery-based component framework, and finally native Web Components in late 2016. The same architectural instincts are what AXS scales today.
act i, before components (2013-2014)
The earliest pattern that survived the next decade: CSS owns the
breakpoint truth; JavaScript subscribes to it. Sounds obvious
now; in 2013 the web hadn’t settled. matchMedia was
inconsistent. Most responsive JS was running its own arithmetic on
window.innerWidth and drifting from the stylesheet.
on-media-query threaded the breakpoint names through
html { font-family }, CSS as the source of
truth, JS as the listener. Components could subscribe to layout contexts
by name (large, xlarge) instead of magic
numbers.
// onMediaQuery, threaded through Skeletor in 2013
//
// CSS owns the breakpoints (in html { font-family }), JS just
// listens for context changes and fires registered callbacks.
// A pre-matchMedia way to keep responsive logic synchronized
// with the stylesheet's source of truth.
mq.addQuery({
context: ['large', 'xlarge'],
match: function() { console.log('desktop layout'); },
unmatch: function() { console.log('leaving desktop'); }
});
mq.listenForChange = function() {
var query_string = window
.getComputedStyle(document.documentElement, null)
.getPropertyValue('font-family');
// Android browsers append a "," to the family list;
// most browsers wrap in quotes. Strip both.
query_string = query_string.replace(/['",]/g, '');
if (query_string !== this.context) {
this.new_context = query_string;
this.triggerCallbacks(this.context, 'unmatch');
this.triggerCallbacks(this.new_context, 'match');
}
this.context = this.new_context;
}; on-media-query/js/onmediaquery.js · 2013
act ii, a framework, on jquery (2016)
By 2016 Delphic was running ~50 client projects on the same component runtime. The problem had shifted from “how do we write components consistently” to “how do we register, load, and tear them down across many pages, breakpoints, and contexts.”
Skeletor.core defined the Plugin.create()
pattern. Every component subclassed a shared base, got a UUID, registered
itself with the global, and surfaced via a kebab-case name derived from
its class. The shape AXS would later codify, here in proto form.
// Skeletor.core, 2016 - the framework runtime.
//
// Components register themselves with the global object via
// Plugin.create(). Each gets a generated UUID, a kebab-case
// name derived from its class name, and a slot in the
// shared registry. Pre-custom-elements - this was the
// pattern we leaned on while waiting for the spec.
function Plugin(element, options, defaultOptions) {
this.options = $.extend(true, {}, defaultOptions, options);
this.$element = element || $(document);
if (typeof this._init !== 'function') {
throw this.name + ' needs an _init method';
}
var pluginName = functionName(this.constructor).toLowerCase();
this.uuid = GetYoDigits(6, pluginName);
Skeletor._uuids.push(this.uuid);
if (!this.$element.data('skeletorPlugin')) {
this.$element.data('skeletorPlugin', this);
}
this._init(element);
}
Plugin.create = function(SubConstructor, prototype) {
SubConstructor.__super__ = Plugin;
for (var key in Plugin.prototype) {
if (!SubConstructor.prototype[key]) {
SubConstructor.prototype[key] = Plugin.prototype[key];
}
}
SubConstructor.prototype = $.extend(true, SubConstructor.prototype, prototype);
SubConstructor.prototype.constructor = SubConstructor;
SubConstructor.prototype.name = functionName(SubConstructor);
var className = SubConstructor.prototype.name;
var attrName = hyphenate(className);
Skeletor._plugins[attrName] = Skeletor[className] = SubConstructor;
}; Skeletor.core/skeletor.core.js · 2016
Skeletor.util.componentLoader scanned the DOM for
[data-component] markers, grouped them by media-query
context, and used on-media-query to lazy-load + initialize
the right components at the right breakpoint, including
destroy() on the way out. AMD-based code splitting before
tree-shaking was a default.
// Skeletor.util.componentLoader, 2016 - dynamic loading.
//
// Scans the DOM for [data-component] elements, groups them by
// [data-component-context] (a media-query name), and uses
// onMediaQuery to load + init the right components at the
// right breakpoint. AMD-based code splitting before tree-
// shaking was a thing every bundler had.
_init: function(element) {
var _this = this;
var components = $.map($('[data-component]'), function(el){
return {
context: $(el).data('component-context') || null,
name: 'components/' + $(el).data('component')
};
});
var contexts = this._mergeByContext(components);
if (!MQ.callbacks) MQ.init();
$.each(contexts, function(i, component) {
if (component.context != 'null') {
_this._addContextQuery(component.context, component.name);
} else {
_this._loadComponent(component.name);
}
});
},
_addContextQuery: function(context, components) {
MQ.addQuery({
context: context.split(','),
call_for_each_context: false,
match: function() {
require(components.split(','), function() {
$.each(arguments, function(i, component) {
component.init();
});
});
},
unmatch: function() {
$.each(components.split(','), function(i, name) {
var c = require(name);
if (typeof c.destroy === 'function') c.destroy();
});
}
});
} Skeletor.util.componentLoader/skeletor.util.componentLoader.js
· 2016
act iii, custom elements (late 2016)
The Web Components v1 spec stabilized in 2018. Lit didn’t exist
yet. Stencil was over a year out. Skeletor picked standards anyway. The
base class was rewritten to extend HTMLElement
directly, with a connectedCallback instead of an
init pattern. From here on, components were custom elements.
// skeletor-plugin-base, December 2016 - the moment we
// stopped emulating components and started writing them.
// extends HTMLElement, connectedCallback - the v1 spec
// before v1 stabilized.
import $ from 'jquery';
import skeletor from 'skeletor-core';
class SkeletorPlugin extends HTMLElement {
constructor(element, options) {
super();
this.NAME = this.constructor.ELEMENT_NAME;
this.UUID = skeletor.getYoDigits(6, this.NAME);
this.$element = $(this) || $(document);
this.options = options;
if (!this.$element.data('skeletorComponent')) {
this.$element.data('skeletorComponent', this);
}
skeletor.registerComponentInstance(this.UUID);
}
// web component v1 spec: Fired when custom element is added to DOM.
connectedCallback() {
this.init();
}
init() {
console.log('connected:', this);
}
static register() {
let elementName = this.ELEMENT_NAME;
if (!elementName) {
throw new TypeError('You need to define an element name for your component');
}
skeletor.registerComponent(this, this.ELEMENT_NAME);
}
set defaults(value) {
this.options = $.extend(true, {}, value, this.options);
}
}
export default SkeletorPlugin; skeletor-plugin-base/src/skeletor-plugin-base.js ·
December 2016
A real component built on the base, the accordion plugin. The
same shape AXS still uses in 2026: subclass the base,
declare defaults, call register() to wire the custom
element into the registry. The jQuery dependency dates it; the
architectural choice doesn’t.
// skeletor-plugin-accordion, 2017 - a real component
// built on the base. extends SkeletorPlugin (which
// extends HTMLElement). Registers itself via the
// static .register() call. The whole pattern AXS
// rebuilt in TypeScript nine years later, already
// running in 2017.
import { skeletor, SkeletorPlugin } from 'skeletor';
class Accordion extends SkeletorPlugin {
constructor(element, options) {
super(element, options);
this.VERSION = '0.1.0';
this.defaults = {
optionOne: true,
optionTwo: false
};
console.log('created a new accordion');
}
}
Accordion.register(); skeletor-plugin-accordion/src/accordion.js · 2017
act iv, accessibility as a baseline (2016)
A sister repo, accessible-components, ran in parallel. It
bound WAI-ARIA Authoring-Practices attributes to the same DOM elements
the framework owned, not as an audit pass after the fact, but as
part of each component’s init. Roles, labels, hidden
state, expanded state, all wired together.
// accessible-components/aria.js, 2016 - WAI-ARIA
// attributes bound to the same DOM elements the
// accordion component owned. Accessibility wasn't a
// separate pass; the binding ran as part of init.
bindAccordion: function($accordion, $heading, $tab, $panel) {
var _ = this;
$($accordion).each(function(i) {
var $this = $(this);
$this.attr('id', 'accordion_' + i);
_.setAccordionAttributes($this, $heading, $tab, $panel, i);
});
},
setAccordionAttributes: function($this, $heading, $tab, $panel, i) {
$this.find($heading).each(function() {
$(this).attr({ 'role': 'heading', 'aria-level': '3' });
});
$this.find($tab).each(function(j) {
$(this).attr({
'id': 'a' + i + '_tab' + j,
'role': 'button',
'aria-expanded': 'false',
'aria-controls': 'a' + i + '_panel' + j
});
});
$this.find($panel).each(function(j) {
$(this).attr({
'id': 'a' + i + '_panel' + j,
'role': 'region',
'aria-hidden': 'true',
'aria-labelledby': 'a' + i + '_tab' + j
});
});
} accessible-components/Static/src/js/components/common/aria.js
· 2016
companion artifacts
A few more sister repos rounded out the story:
- Skeletor.sass.utilities / .helpers / .flexgrid , a Sass-driven token system: spacing primitives, button + input mixins, flexbox grid. Tokens lived in stylesheets because CSS custom properties weren’t broadly safe yet.
- Skeletor.docs, the project’s documentation site. Stories before Storybook was the default.
- dd-pattern-library, 2017, the agency’s pattern-lab experiment. The first version of “design system as an organizational artifact” rather than a per-project boilerplate.
- Skeletor.pluginTemplate, the scaffold an
engineer ran to start a new component. The
/build-v1Claude Code skill on AXS today is the same idea, run by an agent.
what this proved (and what came next)
Skeletor never needed to be a public open-source brand. It was infrastructure for one agency’s client work and did its job well for a few years. What matters now is what it shows:
- Framework-agnostic component architecture isn’t a 2024 trend for me. The pattern that powers AXS, standards- based components, framework-light runtime, design tokens via Sass / CSS properties, is the same pattern Skeletor was running in 2016.
- The instinct to build platforms, not features, is durable. Skeletor was always a system, not a project. The reason the AXS work worked at Vanguard is the same reason Skeletor worked at the agency: separate the substrate from the application code, and let it compound.
- Agency speed compounds. Building on a shared component runtime across ~50 client engagements is what taught me what does and doesn’t survive contact with real delivery pressure. AXS inherited those instincts.
stack
- Components
- Native Web Components (v1 spec), custom elements, jQuery-augmented base class
- Build
- Gulp 3, JSPM, Bower (yes, Bower), Babel, Sass, AMD modules
- Design tokens
- Sass utilities + helpers + flexgrid
- Responsive
- on-media-query, CSS-defined breakpoints, JS subscribers
- Accessibility
- WAI-ARIA Authoring Practices bound at component init
- Distribution
- npm + Delphic’s internal Bower registry
- Documentation
- Skeletor.docs (custom site) + the “Fednet” agency knowledge base
Archived org: github.com/delphic-digital · Related: Building AXS · Framework-Agnostic Architecture