Skyra Survey Widget Integration
Version: 2.0 (2026-01)
Load script:
=== TYPESCRIPT TYPES ===
declare global {
interface Window {
SKYRA_CONFIG?: SkyraConfig
skyra: SkyraAPI
}
}
type SkyraConfig = {
org: string // REQUIRED: organization slug
testMode?: boolean // bypass targeting rules
traits?: Record // user traits for segmentation
cssSelector?: string // mount point (default: 'body')
consent?: boolean // alias for cookieConsent
cookieConsent?: boolean // allow cookies (default: true)
cookieDomain?: string // share cookies across subdomains
autoStart?: boolean // auto-init on load (default: true)
version?: 'v1' | 'v2' // v2 for multiple inline surveys
surveyOverrides?: Record
priority?: number
only?: boolean // show only this survey
}>
}
type SkyraAPI = {
start(config?: Partial): void
reload(): void
setConsent(value: boolean): void
setLanguage(code: string): void
getLanguage(): string | undefined
getAvailableLanguages(): Array<{ code: string; name: string }>
hideSurveys(criteria: { id?: string; slug?: string; renderType?: 'Popup' | 'Inline' }): void
showSurveys(criteria: { id?: string; slug?: string; renderType?: 'Popup' | 'Inline' }): void
preview(params: { slug: string; target?: string }): void
redactPathname(pattern: string): void
redactSearchParam(param: string, options?: { path?: string; paths?: string[] }): void
on(event: 'ready' | 'surveyStarted' | 'surveyCompleted' | 'surveyRejected' | 'cardSaved', handler: (e: any) => void): void
off(event: string, handler: (e: any) => void): void
debugInfo(options?: { format?: 'json' | 'console'; survey?: string }): any
_debugEnabled: boolean
}
type CardSavedEvent = {
type: 'cardSaved'
sessionId: string
visitorId: string
surveyId: string
surveyName: string
cardId: string
cardType: 'TopTaskCard' | 'SegmentCard' | 'LikertCard' | 'RecruitmentCard' | 'InputCard' | 'MessageCard' | 'FindabilityCard' | 'MultiSelectCard' | 'CompletionCard' | 'SingleSelectCard'
value: any
}
=== POPUP SURVEY ===
=== INLINE SURVEY ===
Custom element attributes:
- slug: REQUIRED, format "org/survey-name"
- inline: presence makes it inline (no value needed)
- consent: "true" | "false"
- lang: language code override
=== CONFIG OPTIONS ===
window.SKYRA_CONFIG = {
org: 'your-org', // REQUIRED
consent: false, // set true when user accepts cookies
testMode: false, // bypass targeting rules for dev
cookieConsent: true, // allow cookies
cookieDomain: '.example.com', // share cookies across subdomains
autoStart: true, // set false for manual init
cssSelector: 'body', // popup mount point
version: 'v2', // use v2 for multiple inline surveys
traits: {
userId: '123',
plan: 'pro'
},
surveyOverrides: {
'your-org/specific-survey': {
testMode: true,
priority: 1
}
}
};
=== SCRIPT LOAD DETECTION ===
The script creates window.skyra when loaded. Handle async loading:
// Pattern 1: Wait for script load
function waitForSkyra(callback, timeout = 5000) {
const start = Date.now();
const check = () => {
if (window.skyra) {
callback(window.skyra);
} else if (Date.now() - start < timeout) {
requestAnimationFrame(check);
} else {
console.error('Skyra failed to load');
}
};
check();
}
// Pattern 2: Use skyraStart callback (called automatically after load)
window.skyraStart = function() {
// Called after window.skyra is available
window.skyra.on('ready', () => console.log('Surveys loaded'));
};
// Pattern 3: Check before use (safe for SPAs)
window.skyra?.reload();
=== COOKIE CONSENT ===
Best practice: read existing consent state, then listen for changes.
// Generic pattern
window.skyra.setConsent(cookieManager.hasAnalyticsConsent());
cookieManager.onConsentChange(() => {
window.skyra.setConsent(cookieManager.hasAnalyticsConsent());
});
Cookiebot:
window.skyra.setConsent(Cookiebot.consent?.statistics ?? false);
window.addEventListener('CookiebotOnAccept', () => window.skyra.setConsent(Cookiebot.consent.statistics));
window.addEventListener('CookiebotOnDecline', () => window.skyra.setConsent(false));
OneTrust:
window.skyra.setConsent(OnetrustActiveGroups?.includes('C0002') ?? false);
function OptanonWrapper() {
window.skyra.setConsent(OnetrustActiveGroups.includes('C0002'));
}
CookieYes:
window.skyra.setConsent(getCookieYesConsent());
document.addEventListener('cookieyes_consent_update', () => {
window.skyra.setConsent(getCookieYesConsent());
});
=== JAVASCRIPT API ===
window.skyra.start({ org: 'x' }) // manual init if autoStart: false
window.skyra.reload() // call after SPA navigation
window.skyra.setConsent(true)
window.skyra.setLanguage('nb')
window.skyra.getLanguage()
window.skyra.hideSurveys({ renderType: 'Popup' })
window.skyra.showSurveys({ renderType: 'Popup' })
window.skyra.hideSurveys({ slug: 'org/survey' })
window.skyra.preview({ slug: 'org/survey' })
window.skyra.redactPathname('/users/:id/profile') // privacy: redacts /users/123/profile
window.skyra.redactSearchParam('token')
=== EVENTS ===
window.skyra.on('ready', () => {});
window.skyra.on('surveyStarted', (e) => console.log(e.slug));
window.skyra.on('surveyCompleted', (e) => console.log(e.slug));
window.skyra.on('surveyRejected', (e) => console.log(e.slug));
window.skyra.on('cardSaved', (e) => {
// e.cardType: 'LikertCard' | 'TopTaskCard' | 'SegmentCard' | 'InputCard' | 'RecruitmentCard' | ...
// e.value: response data
// e.sessionId, e.visitorId, e.surveyId, e.cardId
});
Analytics example:
window.skyra.on('surveyCompleted', (e) => {
gtag('event', 'survey_completed', { survey: e.slug });
});
=== SPA INTEGRATION ===
IMPORTANT: Call reload() after route changes to re-evaluate survey targeting.
React (with script load check):
useEffect(() => {
window.skyra?.reload();
}, [location.pathname]);
Next.js App Router:
'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
export function SkyraReloader() {
const pathname = usePathname();
useEffect(() => {
window.skyra?.reload();
}, [pathname]);
return null;
}
Next.js Pages Router:
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export function SkyraReloader() {
const router = useRouter();
useEffect(() => {
window.skyra?.reload();
}, [router.asPath]);
return null;
}
Vue Router:
router.afterEach(() => window.skyra?.reload());
Angular:
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(() => window.skyra?.reload());
=== STYLING ===
All optional. Surveys render with defaults.
skyra-survey {
--skyra-bg-color: #ffffff;
--skyra-text-color: #1a1a1a;
--skyra-action-color: #0066cc;
--skyra-action-text-color: #ffffff;
--skyra-secondary-color: #f5f5f5;
--skyra-interface-color: #666666;
--skyra-z-index: 100;
}
Dark mode:
[data-theme="dark"] skyra-survey {
--skyra-bg-color: #1a1a2e;
--skyra-text-color: #f5f5f5;
}
@media (prefers-color-scheme: dark) {
skyra-survey {
--skyra-bg-color: #1a1a2e;
--skyra-text-color: #f5f5f5;
}
}
Shadow DOM parts for deeper styling:
skyra-survey::part(heading) { }
skyra-survey::part(button-primary) { }
skyra-survey::part(button-secondary) { }
skyra-survey::part(description) { }
=== EDGE CASES ===
Script load failure:
CSP (Content Security Policy) - add these directives:
script-src: https://survey.skyra.no
connect-src: https://ingest.skyra.no https://ingest.staging.skyra.no https://survey.skyra.no
style-src: 'unsafe-inline' (for shadow DOM styles)
frame-src: https://survey.skyra.no (if using iframe mode)
Common errors and fixes:
"[Skyra] No organization specified"
-> Set window.SKYRA_CONFIG = { org: 'your-org' } before script loads
"[Skyra] Could not find DOM element with selector"
-> Ensure cssSelector element exists in DOM when script runs
-> For SPAs, use autoStart: false and call start() after mount
"skyra.reload() called before skyra.start()"
-> Script hasn't initialized yet, use optional chaining: window.skyra?.reload()
Surveys not showing:
1. Check consent: window.skyra.setConsent(true)
2. Check targeting rules in Skyra dashboard
3. Enable debug: window.skyra._debugEnabled = true; window.skyra.debugInfo()
Multiple inline surveys on same page:
-> Use version: 'v2' in SKYRA_CONFIG
=== DEBUG ===
window.skyra._debugEnabled = true;
window.skyra.debugInfo(); // pretty console output
window.skyra.debugInfo({ format: 'json' }); // returns data object
window.skyra.debugInfo({ survey: 'org/survey' }); // filter to specific survey