SledgeSledgeDocs
Customize

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
1CSS first. If it can be done in Custom CSS, do not write JavaScript.
2Widget contents to CSS, host elements to JavaScript.
3Never query Sledge elements at the top level. They do not exist yet.
4Prefer events over observers. Reach for MutationObserver only when no event covers your case.
5Scope observers to a shadow root, never to document or documentElement.
6One guard convention — a data- attribute on nodes, a WeakSet for elements.
7Debounce with requestAnimationFrame, not chained setTimeout delays.
8Namespace everything you inject with sledge-cc-.
9Fail closed. Wrap entry points so a broken customization degrades instead of breaking the page.
10Justify 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

On this page