Refrakt is engineered to degrade, not to break: bad options clamp, missing media
falls back, and a machine without WebGL2 gets a CSS slider with the identical API. When
something still looks wrong, it is almost always one of the situations below — and
almost always announced by a [Refrakt] line in the browser console, so
open the console first.
Blank slider
| Symptom | Cause | Fix |
|---|---|---|
A demo page shows “Build the library first”, or the console
says 404 for refrakt/dist/refrakt.umd.js |
The one-time build has not been run yet. This download ships the source;
refrakt/dist/ is produced by the build |
Run npm install then npm run build inside the
refrakt/ folder — see
Start here, step 2 |
Nothing renders; console says container has no .rf-slide children;
instance is inert |
Slides are missing, use a different class, or are not direct children
of the container (a wrapper div in between breaks detection) |
Make every slide a <figure class="rf-slide"> that is an
immediate child of the data-refrakt element |
Nothing renders; console says element not found for "…" |
The selector passed to new Refrakt() matched nothing — typo, or
the script ran before the element existed |
Fix the selector, or construct after DOMContentLoaded (the UMD
auto-init already waits for it) |
| Raw stacked images, unstyled arrows, no warnings | dist/refrakt.css is not linked — the JS bundle deliberately
injects no CSS |
Add <link rel="stylesheet" href="dist/refrakt.css"> in
<head> |
| Container collapses to zero height | Slides are absolutely positioned, so the container has no intrinsic height | Give the container explicit size in your CSS: height: 100vh, a
fixed height, or aspect-ratio |
| Slider area shows the page background instead of slides | Theme CSS paints an opaque background over the media area — the shared canvas
is a fixed transparent overlay at z-index: 0, so opaque layers
above it hide the rendering |
Remove opaque background from the container, its slides, and any
wrapper that covers the slider’s box |
| Blank only inside a scrolling/overflow panel | An ancestor’s clip rect fully excludes the container (e.g. it sits inside a
collapsed overflow: hidden element) |
Ensure the slider’s box is actually visible inside every clipping ancestor |
CSS fallback engaged
When WebGL2 is unavailable, Refrakt mounts its CSS mode: same markup, same API, same
events — transitions become crossfades with a backdrop-blur glass veil instead of
refraction. This is by design, and it announces itself through the
fallback event, which fires before ready:
const slider = new Refrakt('#hero');
slider.on('fallback', ({ reason }) => {
// reason: 'no-webgl2' | 'forced' | 'context-lost'
console.log('Refrakt is in CSS mode:', reason);
});
| Reason | Meaning | What to do |
|---|---|---|
no-webgl2 |
The browser has no WebGL2 context — an old browser, a blocklisted GPU driver, hardware acceleration disabled in browser settings, or an enterprise policy | Nothing; this is the designed path for that hardware. Verify your own machine
at a WebGL2 test page or check chrome://gpu if you did not expect
it |
forced |
forceFallback: true is set in the configuration |
Remove the option — it exists for testing the fallback and for pages that opt out of GL deliberately (see page-builder conflicts) |
context-lost |
The GPU context was lost at runtime (driver reset, GPU pressure from other tabs) and could not be restored — Refrakt migrated the live instance to CSS mode, keeping its position | Nothing at the page level; a reload gets GL back. Frequent losses on one machine indicate driver or GPU-memory trouble beyond the page |
Users with prefers-reduced-motion do not get the fallback —
they get the GL engine with motion effects reduced per the
a11y.reducedMotion option (crossfades, no tear).
Images stay low-res / frosted
| Symptom | Cause | Fix |
|---|---|---|
A slide stays a frosted blur; console shows slide N media failed:
<url> |
The data-rf-src path 404s. Paths resolve relative to the
page URL, not the script or CSS location |
Fix the path; check the failing URL in the console warning, or the Network panel |
Console shows slide N decode timeout |
The full-resolution file took longer than 8 s to fetch and decode — usually a multi-MB original on a slow connection | Export at 1600 px longest edge per the media guide; the LQIP remains up meanwhile |
| CDN-hosted images never sharpen | Missing CORS header — WebGL cannot sample a cross-origin image without
Access-Control-Allow-Origin |
Configure the header on the CDN (how), or serve media from the page’s own origin |
data-rf-srcset always picks the wrong size, or is ignored |
Density descriptors (2x) — only width descriptors
(800w) are supported. Or the container was resized after load:
selection is one-shot by design |
Use width descriptors; author candidates around real container widths |
| Image is sharp but low-res on hi-DPI displays, no warnings | Only the inline src (the LQIP) was provided — it is then both
placeholder and final image |
Add data-rf-src with the full-resolution file |
| A slide renders as a flat ink-colored panel | Both LQIP and full-resolution failed (slide N has no media /
media failed warnings) — the ink panel is the terminal
fallback |
Fix the media paths; the slide is intentionally kept in the sequence so indices and deep links stay stable |
Video won’t play
| Symptom | Cause | Fix |
|---|---|---|
| Poster shows but playback never starts | Browser autoplay policy blocked the muted play() (rare — usually
data-saver or an embedded webview) |
Nothing required: Refrakt retries on the visitor’s next pointer interaction.
Never set media.video.muted to false expecting
autoplay with sound — no browser allows it, and Refrakt forces mute while
autoplaying |
| Video plays, then pauses “randomly” | By design: a video plays only while its slide is centered and the tab is visible; leaving center pauses it (it resumes, not restarts) | None — this is the §media playback model and saves battery/decoders |
Works in Safari, dead in Chrome/Firefox; console shows
slide N video failed |
HLS (.m3u8) source — natively supported only by Safari/iOS; no
hls.js is bundled (zero-dependency rule) |
Provide H.264 MP4 — the universal format (see encoding requirements) |
Console shows slide N video failed: <url> and the poster
image is used |
404, unsupported codec (e.g. HEVC in Firefox), or a CORS-blocked video host | Check the URL, re-encode to H.264 + AAC, or fix CORS (notes) |
| Some video slides show stills on a video-heavy page | The shared decoder pool holds at most 4 <video> elements;
furthest-from-center slides release theirs and show posters |
None — always supply data-rf-poster so the still is intentional |
| YouTube/Vimeo slide never plays “inline” | Not a bug: cross-origin iframes cannot be WebGL textures. Provider slides show a refractable poster; playback opens in the lightbox | That is the designed behavior — see the poster pattern |
Strict CSP pages
Refrakt is CSP-friendly by construction: no eval, no Web Workers, no
runtime style injection, no inline scripts, no external fetches for its own code. What a
Content-Security-Policy needs to allow depends only on where your
media lives:
| Directive | Needed? | Value |
|---|---|---|
script-src |
Yes | Your own host (wherever refrakt.umd.js /
refrakt.esm.js is served). No 'unsafe-inline', no
'unsafe-eval' required |
style-src |
Yes | Your own host for refrakt.css. Refrakt injects no
<style> elements, so no inline-style allowance is
required |
img-src |
Yes | Every host serving slide images. Add i.ytimg.com only if
you rely on YouTube poster derivation instead of
data-rf-poster |
media-src |
If using video | Every host serving data-rf-video MP4 files |
frame-src |
If using YouTube/Vimeo slides | www.youtube-nocookie.com and/or
player.vimeo.com — used only by lightbox playback |
connect-src |
Only without data-rf-poster |
vimeo.com for oEmbed poster derivation. Supply
data-rf-poster on Vimeo slides and this entry is unnecessary |
worker-src |
No | Refrakt uses no workers |
Fully self-hosted pages (own images, own MP4s, data-rf-poster on
provider slides) run under
default-src 'self' with no exceptions at all.
Page-builder conflicts
Refrakt draws all instances on one shared <canvas> appended to
<body> with position: fixed; z-index: 0 — a transparent
overlay that paints only inside each slider’s rectangle. Page builders and theme
frameworks interact with that design in two known ways:
Z-index stacking
The canvas sits at z-index: 0 in the root stacking context; slide text
(z: 10) and navigation (z: 20) stack above it. Two things can
go wrong:
- Content hidden behind the slider’s pixels: a theme element with
z-index: -1or auto-stacking under the canvas will be covered inside the slider’s rect. Give page content a normal (≥ 0) stacking position. - Slider pixels hidden behind content: builders that wrap sections in opaque, positioned containers can paint over the canvas. Keep the slider’s section background transparent where the media should show.
The transform containing-block limitation
Known limitation: if any ancestor of the canvas has a
transform, filter, backdrop-filter,
perspective, or will-change: transform, CSS makes that
ancestor the containing block for position: fixed descendants. The shared
canvas then stops being viewport-fixed — it scrolls and offsets with that ancestor, and
the rendered slides drift out of alignment with their containers.
This is a CSS specification behavior, not a bug Refrakt can code around. It typically
appears with smooth-scrolling libraries that translate a page wrapper, and with
builder “page transition” or zoom effects that put a transform on body or a
top-level wrapper that ends up containing the canvas.
Fixes, in order of preference:
- Remove the transform/filter from the ancestor. Scope animations
to sections that do not contain the slider; prefer
opacity-based transitions, which do not create containing blocks. - Accept the CSS fallback on pages where the ancestor transform
is non-negotiable: set
"forceFallback": trueindata-rf-options. CSS mode renders entirely inside the container — no fixed canvas, no containing-block sensitivity — with the same API and events.
Console warning glossary
Every warning Refrakt can emit, verbatim. Each fires at most once per cause, is
prefixed [Refrakt], and never accompanies an exception — warnings are
advice, not failures.
| Warning | Meaning & fix |
|---|---|
element not found for "<selector>"; instance is inert |
The constructor selector matched nothing. The instance is a safe no-op stub. Fix the selector or construction timing |
container has no .rf-slide children; instance is inert |
No direct .rf-slide children found. Check the markup contract
(blank slider) |
element already bound; returning the existing instance |
new Refrakt() was called twice on one element (often auto-init
plus a manual call). Harmless — you received the original instance |
data-rf-options ignored: <parse error> |
The attribute is not valid JSON — usually single-quoted keys or a trailing comma. The preset still applies; the overrides do not |
unknown preset "<name>"; using defaults |
No preset registered under that name. Check spelling against
Refrakt.presets, or register your custom preset before
constructing |
unknown option "<path>" ignored |
The option path is not in the schema — typo or wrong nesting. See the options reference in the reference manual |
<path> <value> clamped to <limit> |
The value was outside its documented range and was clamped to the nearest bound. The slider runs with the clamped value |
<path> invalid value <v>; using default <d> |
Wrong type for the option (e.g. a string where a number belongs). The default is used |
"<path>" requires rebuild(); stored, not applied |
setOption() was called on a non-live option (like
layout.mode). The value is stored and takes effect on the next
rebuild() |
preset "<name>" overwritten |
registerPreset() replaced an existing name. Intentional
overrides can ignore this |
registerPreset: invalid preset name <name> |
The name passed to registerPreset() is empty or not a string.
The call was ignored |
instance destroyed; further calls are ignored |
A method was called after destroy(). All post-destroy calls are
safe no-ops; this reminds you once |
non-numeric index <i> ignored |
goTo() / openLightbox() received something that is
not a finite number |
index out of range; wrapped to <i> /
clamped to <i> |
The index exceeded the slide count — wrapped when
layout.loop is on, clamped when off |
unknown event "<name>" |
on() received an event name outside the documented set (check
spelling: ready, change, drag,
transitionstart, transitionend,
mediaplay, lightboxopen,
lightboxclose, fallback, destroy) |
on("<name>") requires a function |
The second argument to on() was not a function; the subscription
was ignored |
slide <n> has no media |
The slide has no <img>, no data-rf-src, no
data-rf-video, and no provider attribute. It renders as an
ink-colored panel and stays in the sequence |
slide <n> media failed: <url> |
The full-resolution image 404ed or failed to decode. The LQIP stays up if present, else the ink panel. Fix the URL (details) |
slide <n> decode timeout: <url> |
Fetch + decode exceeded 8 s. Usually an oversized source file — resize per the media guide |
slide <n> video failed: <url> |
The video errored or its codec is unsupported. Falls back to
data-rf-poster, else the ink panel
(details) |
slide <n> has no full-size media; lightbox not opened |
openLightbox() targeted a slide whose media failed or never
existed — there is nothing sharp to show |
unrecognized video URL |
data-rf-video holds a URL that is neither a media file nor a
parseable YouTube/Vimeo link. Use the bare-ID attributes
(data-rf-youtube / data-rf-vimeo) instead |
Vimeo oEmbed failed for <id>; supply data-rf-poster |
Poster derivation could not reach Vimeo (offline, private video, provider
change). Add data-rf-poster — required for Vimeo slides
(why) |
shader program failed: <log> |
The GPU driver rejected the shader — extremely rare, usually a broken driver. The instance falls back to CSS mode; please report the console log if you see this on current hardware |