Org Chart Grow
An org chart that branches out from the top: the top box pops in, elbow lines grow down to the next level, and each box pops in as its line arrives. On a narrow screen the bottom level of each branch stacks into a column so the chart still fits.
- Chief Executive
- Design
- Brand
- Product
- Engineering
- Web
- Apps
- Data
- Sales
- Europe
- Americas
- Design
How to use it
- Paste the snippet just before
</head>. - Write the chart as a nested list: one
<li>at the top, with a<ul>of its reports inside, and so on. - Add
data-org-chart-growto the outer<ul>.
Options
Add any of these attributes to the same element.
data-color | Colour of the boxes and lines. Default #a855f7. |
data-level | Time to grow each level. Default 700 ms. |
data-loop | Fade out and grow again. |
Text size follows the list’s font-size. Long names shrink to fit their box.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Org Chart Grow: an org chart that branches out from the top. The top box
// pops in, elbow lines grow down to the next level, those boxes pop in, and
// so on down the tree. Write it as a nested list. When the chart is too
// wide for its box (on a phone, say), the bottom level of each branch
// stacks into a column so it still fits.
// Usage:
// <ul data-org-chart-grow>
// <li>CEO
// <ul>
// <li>Design <ul><li>Brand</li><li>Product</li></ul></li>
// <li>Engineering <ul><li>Web</li><li>Apps</li></ul></li>
// </ul>
// </li>
// </ul>
// Text size follows the element's font-size.
// Optional: data-color (default #a855f7), data-level (ms per level, default
// 700), data-loop (fade out and grow again).
var DEFAULTS = { color: '#a855f7', level: 700, hold: 2600, ink: '#1e1b2e' };
var NS = 'http://www.w3.org/2000/svg';
var css =
'.ocg{display:flex;align-items:center;justify-content:center;list-style:none;padding:0}' +
'.ocg-svg{display:block;width:100%;height:auto;max-height:100%;overflow:visible}' +
'.ocg-node{transform-box:fill-box;transform-origin:50% 50%}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function mk(tag, attrs, parent) {
var n = document.createElementNS(NS, tag);
for (var k in attrs) n.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(n);
return n;
}
// Centred text, shrunk to fit maxW.
function label(parent, text, x, y, maxW, fs, fill) {
var t = mk('text', { x: x, y: y, 'text-anchor': 'middle', 'dominant-baseline': 'central', 'font-size': fs, fill: fill, 'font-weight': 600 }, parent);
t.textContent = text;
var w = 0;
try { w = t.getComputedTextLength(); } catch (e) {}
w = w || text.length * fs * 0.56;
if (w > maxW) t.setAttribute('font-size', Math.max(fs * 0.62, (fs * maxW) / w));
}
function parseLi(li) {
var text = '', sub = null;
li.childNodes.forEach(function (c) {
if (c.nodeType === 1 && (c.tagName === 'UL' || c.tagName === 'OL')) sub = sub || c;
else text += c.textContent;
});
return { text: text.replace(/\s+/g, ' ').trim(), kids: sub ? Array.from(sub.children).map(parseLi) : [] };
}
function slots(n, compact) {
n.stack = false;
if (!n.kids.length) return (n.slots = 1);
if (compact && n.kids.every(function (k) { return !k.kids.length; })) { n.stack = true; return (n.slots = 1); }
return (n.slots = n.kids.reduce(function (a, k) { return a + slots(k, compact); }, 0));
}
function render(el) {
var s = el._ocg, d = el.dataset;
var color = d.color || DEFAULTS.color;
var W = el.clientWidth || 320;
var fs = parseFloat(getComputedStyle(el).fontSize) || 14;
s.w = el.clientWidth;
var tree = s.tree;
var total = slots(tree, false);
if (W / total < fs * 6.4) total = slots(tree, true);
var slotW = W / total;
var h = fs * 2.5, w = Math.min(slotW - fs * 0.8, fs * 9.5), levelH = fs * 5.2;
var indent = fs * 1.4, stackGap = fs * 0.7;
el.textContent = '';
var svg = mk('svg', { class: 'ocg-svg', 'aria-hidden': 'true' }, el);
var root = mk('g', {}, svg);
var lines = mk('g', { fill: 'none', stroke: color, 'stroke-width': 1.6, 'stroke-opacity': 0.7, 'stroke-linejoin': 'round' }, root);
var boxes = mk('g', {}, root);
var maxY = 0;
s.nodes = [];
s.links = [];
// Place each node, then give it a box, and a link to its parent.
function place(n, x0, y, depth, nw) {
n.x = x0 + (n.slots * slotW) / 2;
n.y = y;
// Managers may be wider than one slot when they have room.
nw = Math.max(nw, Math.min(n.slots * slotW - fs * 0.8, fs * 11));
draw(n, depth, nw);
if (n.stack) {
n.kids.forEach(function (k, j) {
k.x = n.x + indent / 2;
k.y = n.y + (j + 1) * (h + stackGap);
draw(k, depth + 1, nw - indent);
var rail = n.x - nw / 2 + indent / 2;
link(n, k, 'M' + rail + ',' + (n.y + h / 2) + ' V' + k.y + ' H' + (k.x - (nw - indent) / 2), j);
});
} else {
var x = x0;
n.kids.forEach(function (k, j) {
var kx = x + (k.slots * slotW) / 2, ky = y + levelH, mid = y + h / 2 + (levelH - h) / 2;
link(n, k, 'M' + n.x + ',' + (y + h / 2) + ' V' + mid + ' H' + kx + ' V' + (ky - h / 2), 0);
place(k, x, ky, depth + 1, w);
x += k.slots * slotW;
});
}
}
function draw(n, depth, nw) {
var g = mk('g', { class: 'ocg-node' }, boxes);
var top = depth === 0;
mk('rect', { x: n.x - nw / 2, y: n.y - h / 2, width: nw, height: h, rx: fs * 0.55, fill: top ? color : '#fff', stroke: color, 'stroke-width': top ? 0 : 1.6, 'stroke-opacity': depth > 1 ? 0.55 : 1 }, g);
label(g, n.text, n.x, n.y, nw - fs * 0.9, fs, top ? '#fff' : DEFAULTS.ink);
maxY = Math.max(maxY, n.y + h / 2);
n.g = g;
s.nodes.push(n);
}
function link(p, k, path, j) {
s.links.push({ from: p, to: k, path: mk('path', { d: path }, lines), order: j });
}
place(tree, 0, h / 2 + 2, 0, w);
var H = Math.ceil(maxY + 4);
svg.setAttribute('viewBox', '0 0 ' + W + ' ' + H);
svg.setAttribute('width', W);
svg.setAttribute('height', H);
s.root = root;
}
function hide(s) {
anime.utils.set(s.root, { opacity: 1 });
s.nodes.forEach(function (n) { anime.utils.set(n.g, { opacity: 0, scale: 0.5 }); });
s.links.forEach(function (l) { anime.utils.set(anime.svg.createDrawable(l.path), { draw: '0 0' }); });
}
function start(el) {
var s = el._ocg;
var level = +el.dataset.level || DEFAULTS.level;
hide(s);
var tl = anime.createTimeline({
onComplete: function () {
if (s.tl !== tl) return;
if (s.loop) start(el); else s.done = true;
}
});
s.tl = tl;
var end = 0;
// Each box pops at its own time; its links grow after it, and each child
// pops as its link arrives.
function grow(n, t) {
tl.add(n.g, { opacity: [0, 1], scale: [0.5, 1], duration: 450, ease: 'outBack(1.7)' }, t);
end = Math.max(end, t + 450);
s.links.forEach(function (l) {
if (l.from !== n) return;
var dur = level * 0.55 + l.order * 110;
var t0 = t + level * 0.3;
tl.add(anime.svg.createDrawable(l.path), { draw: ['0 0', '0 1'], duration: dur, ease: 'inOut(2)' }, t0);
grow(l.to, t0 + dur - 90);
});
}
grow(s.tree, 0);
if (s.loop) tl.add(s.root, { opacity: [1, 0], duration: 500, ease: 'in(2)' }, end + DEFAULTS.hold);
}
function setup(el) {
var first = el.querySelector('li');
if (!first) return;
var s = (el._ocg = { tree: parseLi(first), loop: on(el.dataset.loop), tl: null, visible: false, done: false });
el.classList.add('ocg');
el.setAttribute('role', 'img');
if (!el.getAttribute('aria-label')) {
var names = [];
(function walk(n) { names.push(n.text); n.kids.forEach(walk); })(s.tree);
el.setAttribute('aria-label', 'Org chart: ' + names.join(', '));
}
render(el);
if (reduced()) {
new ResizeObserver(function () { if (Math.abs(el.clientWidth - s.w) > 1) render(el); }).observe(el);
return;
}
hide(s);
new ResizeObserver(function () {
if (Math.abs(el.clientWidth - s.w) <= 1) return;
if (s.tl) s.tl.pause();
s.tl = null;
render(el);
if (s.done) return;
hide(s);
if (s.visible) start(el);
}).observe(el);
new IntersectionObserver(function (entries) {
s.visible = entries[0].isIntersecting;
if (s.visible) { if (!s.tl) start(el); else if (!s.done) s.tl.resume(); }
else if (s.tl && !s.done) s.tl.pause();
}, { threshold: 0.2 }).observe(el);
}
function init() {
document.querySelectorAll('[data-org-chart-grow]').forEach(function (el) {
if (el._ocg) return;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><ul data-org-chart-grow>
<li>CEO
<ul>
<li>Design <ul><li>Brand</li><li>Product</li></ul></li>
<li>Engineering <ul><li>Web</li><li>Apps</li></ul></li>
</ul>
</li>
</ul>