Cookbook
Short, self-contained recipes for common integration tasks, all plain JavaScript and CSS with no framework or build step. Each one states which export options it needs.
Highlight every atom of an element on hover
Problem: hovering any oxygen should visually pick out all the oxygens in the structure.
Requires: metadata export option (for data-mk-element).
<style> /* CSS-only variant: hovering one O dims everything that is not an O */ .mol:has(.atom-group[data-mk-element="O"]:hover) .atom-group:not([data-mk-element="O"]), .mol:has(.atom-group[data-mk-element="O"]:hover) .bond-group { opacity: 0.25; transition: opacity 0.15s; }
/* Used by the JS variant below */ .atom-group.is-highlighted { filter: drop-shadow(0 0 3px #fab005); }</style>
<div class="mol"><!-- inline SVG export here --></div>
<script> // JS variant: works in any browser and for any element, not just O document.querySelectorAll('.mol .atom-group[data-mk-element]').forEach(atom => { const symbol = atom.dataset.mkElement; const peers = () => document.querySelectorAll(`.mol .atom-group[data-mk-element="${symbol}"]`); atom.addEventListener('pointerenter', () => peers().forEach(a => a.classList.add('is-highlighted'))); atom.addEventListener('pointerleave', () => peers().forEach(a => a.classList.remove('is-highlighted'))); });</script>How it works: every atom is a g.atom-group carrying data-mk-element, so a single attribute selector matches all atoms of one element. The CSS variant dims non-matches instead of recoloring matches because theme-aware exports set element colors with !important, which a plain fill override would lose to; opacity and filter sidestep that entirely. The CSS variant needs :has() support, so the JS variant doubles as the fallback.
Tooltip from atom metadata
Problem: show hybridization, geometry, and formal charge in a tooltip that follows the cursor.
Requires: metadata export option.
<style> .atom-tip { position: fixed; z-index: 10; pointer-events: none; background: #222; color: #fff; padding: 4px 8px; border-radius: 4px; font: 13px/1.4 system-ui; white-space: pre-line; }</style>
<script> const tip = document.createElement('div'); tip.className = 'atom-tip'; tip.hidden = true; document.body.appendChild(tip);
const svg = document.querySelector('.mol svg');
svg.addEventListener('pointerover', e => { const atom = e.target.closest('[data-mk-element]'); if (!atom) return; const d = atom.dataset; const lines = [d.mkElement]; if (d.mkHybridization) lines.push('Hybridization: ' + d.mkHybridization); if (d.mkGeometry) lines.push('Geometry: ' + d.mkGeometry); if (d.mkFormalCharge && d.mkFormalCharge !== '0') lines.push('Formal charge: ' + d.mkFormalCharge); tip.textContent = lines.join('\n'); tip.hidden = false; });
svg.addEventListener('pointermove', e => { tip.style.left = e.clientX + 14 + 'px'; tip.style.top = e.clientY + 14 + 'px'; });
svg.addEventListener('pointerout', e => { if (e.target.closest('[data-mk-element]')) tip.hidden = true; });</script>How it works: a single delegated pointerover listener on the SVG catches hovers on any atom via closest(), so it keeps working if you swap the molecule out. Hybridization and geometry are conditional attributes (Molkit omits them when the model has nothing to say), so each line is guarded before it is added.
Build your own structure inspector
Problem: you want a small teaching panel where clicking an atom or bond shows its chemistry.
Molkit exports carry enough chemistry to drive small interactive teaching tools without any server or chemistry library: the per-atom and per-bond data-mk-* attributes are the whole data layer.
Requires: metadata export option.
<div class="mol-row" style="display: flex; gap: 1rem;"> <div id="mol"><!-- inline SVG export with metadata here --></div> <aside id="inspector" style="min-width: 14rem;">Click an atom or bond.</aside></div>
<script> const panel = document.getElementById('inspector');
function row(label, value) { return value == null ? '' : `<dt>${label}</dt><dd>${value}</dd>`; }
function renderAtom(d) { panel.innerHTML = `<h3>${d.mkElement} atom</h3><dl>` + row('Formal charge', d.mkFormalCharge) + row('Lone pairs', d.mkLonePairs) + row('Hybridization', d.mkHybridization) + row('Geometry', d.mkGeometry) + row('Octet', d.mkOctet) + '</dl>'; }
function renderBond(d) { const names = { 1: 'single', 2: 'double', 3: 'triple' }; const polarity = d.mkPolarity ? `${d.mkPolarity} (toward ${d.mkPolarToward})` : null; panel.innerHTML = '<h3>Bond</h3><dl>' + row('Order', names[d.mkOrder] ?? d.mkOrder) + row('Stereo', d.mkStereo) + row('Polarity', polarity) + '</dl>'; }
document.querySelectorAll('#mol .atom-group[data-mk-element]').forEach(a => a.addEventListener('click', () => renderAtom(a.dataset)));
document.querySelectorAll('#mol .bond-group[data-mk-order]').forEach(b => b.addEventListener('click', () => renderBond(b.dataset)));</script>How it works: atoms are queried by data-mk-element and bonds by data-mk-order, the two attributes present on every metadata-enabled group; everything else is conditional, which is why row() drops null values instead of printing “undefined”. The octet value is an enum (satisfied, deficient, or expanded) and polarity only appears on meaningfully polar bonds. From here it is a short step to quizzes (“predict the hybridization, then click to check”) or step-through Lewis-structure reasoning.
Static-to-animated toggle
Problem: show the static structure by default and play its animated companion on demand.
Requires: both exports inlined on the page, for example reaction.svg and reaction-animation.svg.
<div id="reaction"> <span class="static"><!-- inline reaction.svg --></span> <span class="animated" hidden><!-- inline reaction-animation.svg --></span></div><button id="play">Play animation</button>
<script> const staticView = document.querySelector('#reaction .static'); const animView = document.querySelector('#reaction .animated'); const btn = document.getElementById('play'); let playing = false;
btn.addEventListener('click', () => { playing = !playing; staticView.hidden = playing; animView.hidden = !playing; btn.textContent = playing ? 'Show static' : 'Play animation'; if (playing) { const svg = animView.querySelector('svg'); svg.setCurrentTime(0); // restart from the first state svg.unpauseAnimations(); } });</script>How it works: animated exports run on a single SMIL timeline, so setCurrentTime(0) plus unpauseAnimations() is a full restart; without the rewind, re-opening the animation would resume mid-timeline. The static sibling stays in the DOM so toggling back is instant. See Animations for the full control surface.
Site theme toggle wiring
Problem: your site has its own light/dark toggle, and embedded SVGs should follow it instead of the OS preference.
Requires: theme-aware exports from a current Molkit build (older theme-aware exports only respond to the OS media query).
const wrapper = document.querySelector('main');
function syncMolkitTheme(dark) { wrapper.classList.toggle('mk-dark', dark); wrapper.classList.toggle('mk-light', !dark);}
// Call from your existing toggle; for example:syncMolkitTheme(document.documentElement.dataset.theme === 'dark');How it works: every theme-aware export ships its dark rules under two hooks: the prefers-color-scheme media query, and an mk-dark class on the SVG or any ancestor that forces dark unconditionally. The mk-light class is the opposite hook, suppressing the media query so a forced-light site never gets OS-dark embeds. Putting both classes on one shared wrapper themes every embed on the page at once; Theming explains the generated CSS.
Feed the structure to RDKit.js
Problem: you want real cheminformatics (canonical SMILES, descriptors, substructure search) on the embedded structure, in the browser.
Requires: metadata export option (for the embedded MOL block). Loading RDKit.js from a CDN is a network dependency of roughly 9 MB of WebAssembly, so lazy-load it.
<script src="https://unpkg.com/@rdkit/rdkit/dist/RDKit_minimal.js"></script><script> function molBlockFrom(svg) { const meta = svg.querySelector('metadata'); if (!meta) return null; for (const block of meta.children) { if (block.localName !== 'chemistry') continue; for (const child of block.children) { if (child.localName === 'mol-block') return child.textContent; } } return null; }
initRDKitModule().then(RDKit => { const molBlock = molBlockFrom(document.querySelector('.mol svg')); if (!molBlock) return; const mol = RDKit.get_mol(molBlock); console.log(mol.get_smiles()); // canonical SMILES console.log(mol.get_descriptors()); // computed properties (JSON) mol.delete(); // RDKit objects are manual-free });</script>How it works: metadata-enabled exports embed a standard V2000 MOL block inside the SVG’s metadata element, in a namespaced mk:chemistry block. The lookup matches on localName rather than tag name because namespaced selectors behave differently across browsers. Once you have the MOL block as text, anything that reads V2000 will take it; RDKit.js is just the most convenient in-browser consumer.
Keyboard step-through presenter
Problem: present an animated mechanism one state at a time with the arrow keys instead of letting it play through.
Requires: an animated export, inlined.
<div class="anim"><!-- inline reaction-animation.svg --></div><p id="anim-status" aria-live="polite"></p>
<script> const svg = document.querySelector('.anim svg'); const status = document.getElementById('anim-status'); const breakpoints = svg.dataset.breakpoints.split(',').map(Number); let state = 0;
function seek(i) { state = Math.max(0, Math.min(i, breakpoints.length - 1)); svg.setCurrentTime(breakpoints[state]); status.textContent = `State ${state + 1} of ${breakpoints.length}`; }
svg.pauseAnimations(); seek(0);
document.addEventListener('keydown', e => { if (e.key === 'ArrowRight') seek(state + 1); if (e.key === 'ArrowLeft') seek(state - 1); });</script>How it works: animated exports stamp data-breakpoints on the root, a comma-separated list of timeline seconds at which each state has fully settled, starting with 0 for the initial state. Pausing the timeline and calling setCurrentTime with a breakpoint renders that state as a still frame, so arrow keys become discrete state navigation. The companion data-duration and data-transitions attributes support richer players; see Animations.
See also
- Overview for the three-layer picture of what an export contains
- Embedding for inlining, sizing, and multiple-copy pitfalls
- Theming for the
mk-*class reference and the dark-mode CSS - Metadata for the full
data-mk-*schema these recipes read - Animations for the SMIL timeline and player patterns