Custom code
Add your own CSS and JavaScript to Sledge widgets. Covers where custom code runs, why CSS cannot reach host elements, and the standard patterns for doing it safely.
Sledge has two settings for your own code: Custom CSS and Custom JavaScript. Both live in the Sledge dashboard under global settings, and both apply across your whole storefront.
Three facts govern everything on this page. Read them before writing anything.
1. Custom CSS is applied inside every Sledge widget's shadow root, at the highest priority.
2. Custom CSS therefore cannot style the widget elements themselves — only their contents.
3. Custom JavaScript runs before any widget has rendered.
1. Custom CSS goes inside widgets
Sledge widgets render in shadow roots, which is what stops theme CSS and Sledge CSS interfering with each other. Your custom CSS is adopted directly into each of those shadow roots, above Sledge's own styles.
So this works — it targets content inside a widget:
.product-card {
border: 1px solid #dedede;
border-radius: 6px;
}And this does not work, because sledge-wishlist-badge is the widget element itself, which lives in your theme's DOM, not inside a shadow root:
/* Never matches. */
sledge-wishlist-badge {
align-self: center;
}2. Host elements need a document-level style
To style the widget element itself — its alignment in a header row, its width in a grid, its margins — inject a document-level stylesheet from Custom JavaScript:
SledgeCC.style('badge-align', 'sledge-wishlist-badge{align-self:center;vertical-align:middle}')This is the single most common reason custom CSS "does nothing". If your selector targets a sledge-* element, it belongs in JavaScript.
3. JavaScript runs before widgets exist
Custom JavaScript executes while Sledge is booting, which is before any widget has rendered. Anything that queries for a Sledge element at the top level will find nothing:
// Always null. Runs too early.
const card = document.querySelector('sledge-product-card')Do the work inside an event handler instead:
document.addEventListener('sledge:product-card:rendered', (e) => {
const card = e.detail?.card
// ...
})A re-render rebuilds the widget's shadow tree and discards your changes, so this event fires again. Every patch must re-apply on each fire — never assume it runs once.
The standard helpers
Paste this at the top of your Custom JavaScript. It handles the patterns above correctly so you don't have to reimplement them.
/* Sledge custom-code helpers */
const SledgeCC = (() => {
const wired = new WeakSet()
/** Inject a document-level stylesheet once. For host elements and theme markup. */
const style = (id, css) => {
const key = `sledge-cc-${id}`
if (document.getElementById(key)) return
const el = document.createElement('style')
el.id = key
el.textContent = css
;(document.head || document.documentElement).appendChild(el)
}
/** Run fn for every product card, now and on every re-render. */
const onCard = (fn) => {
document.addEventListener('sledge:product-card:rendered', (e) => {
const card = e.detail?.card
if (card) try { fn(card) } catch (err) { console.warn('[sledge-cc]', err) }
})
}
/** Observe one element's shadow root, debounced to one frame. Wires once. */
const watch = (el, fn) => {
if (!el?.shadowRoot || wired.has(el)) return
wired.add(el)
let scheduled = false
const run = () => {
scheduled = false
try { fn(el) } catch (err) { console.warn('[sledge-cc]', err) }
}
new MutationObserver(() => {
if (scheduled) return
scheduled = true
requestAnimationFrame(run)
}).observe(el.shadowRoot, { childList: true, subtree: true, characterData: true })
run()
}
/** Find elements across nested shadow roots. */
const findAll = (selector, root = document) => {
const out = []
const walk = (node) => {
out.push(...node.querySelectorAll(selector))
node.querySelectorAll('*').forEach((el) => el.shadowRoot && walk(el.shadowRoot))
}
try { walk(root) } catch (err) { console.warn('[sledge-cc]', err) }
return out
}
/** Mark a node processed. Returns false if it already was. */
const claim = (node, tag) => {
const attr = `data-sledge-cc-${tag}`
if (node.hasAttribute(attr)) return false
node.setAttribute(attr, '1')
return true
}
return { style, onCard, watch, findAll, claim }
})()A worked example
Two of the most common requests together — rename the automatic sale badge, and align the wishlist badge in a header icon row:
/* Purpose: sale badge reads "ON SALE"; header wishlist badge sits on the icon row baseline
* Depends: sledge:product-card:rendered
* Fragile: .badges .badge is internal markup and may change between releases
*/
// Host element — custom CSS cannot reach this, so it goes document-level.
SledgeCC.style('badge-align', 'sledge-wishlist-badge{align-self:center;vertical-align:middle}')
// Widget content — re-applied on every render, because a re-render wipes the tree.
SledgeCC.onCard((card) => {
card.shadowRoot.querySelectorAll('.badges .badge').forEach((b) => {
if (!SledgeCC.claim(b, 'onsale')) return
if (b.textContent.trim().startsWith('-')) b.textContent = 'ON SALE'
})
})Rules
Follow these and your customizations will survive theme changes, re-renders and Sledge updates.
| Rule | |
|---|---|
| 1 | CSS first. If it can be done in Custom CSS, do not write JavaScript. |
| 2 | Widget contents to CSS, host elements to JavaScript. |
| 3 | Never query Sledge elements at the top level. They do not exist yet. |
| 4 | Prefer events over observers. Reach for MutationObserver only when no event covers your case. |
| 5 | Scope observers to a shadow root, never to document or documentElement. |
| 6 | One guard convention — a data- attribute on nodes, a WeakSet for elements. |
| 7 | Debounce with requestAnimationFrame, not chained setTimeout delays. |
| 8 | Namespace everything you inject with sledge-cc-. |
| 9 | Fail closed. Wrap entry points so a broken customization degrades instead of breaking the page. |
| 10 | Justify every !important with a comment naming the inline style it overrides. |
Using !important
Some values are set inline on elements by Sledge, and inline styles cannot be overridden by a stylesheet without !important. That makes it legitimate here — but note why, so a later reader knows it was necessary rather than a guess:
/* !important: the badge's background-color is set inline on the element. */
.product-card .badges .badge {
background: #43c6ac !important;
}What is supported
Supported. The Custom CSS and Custom JavaScript settings, document-level style injection, document-level events, design tokens, and element tag names.
Escape hatch. Reaching into shadowRoot and matching internal class names, as the worked example does with .badges .badge. Internal markup can change in any release. Note it in a comment at the top of your file so you know where to look if a release changes something.
Where an escape hatch is your only option, that is a gap worth reporting — see Support.
Last updated on
Customize
Make Sledge match your store exactly — colors and fonts, your own CSS and JavaScript, or replace a widget's markup with your theme's own.
Styling
How Sledge styling is layered — base styles, a bridge that inherits your theme's colors and fonts, then your own CSS on top — and how to change each layer.