Skip to content

Front-end JavaScript API

How the Quizably player mounts, what window.Quizably and QUIZABLY_FRONTEND expose, registerTemplate and registerQuestionType, and CSS custom properties.

The quiz player is a Vue 3 application that mounts on server-rendered placeholders. Its public JavaScript surface is small: a window.Quizably object with two registration functions, a localized settings object, and a set of CSS custom properties. This page documents what exists in plugin version 1.2.0, and says where something is deliberately not available.

What does not exist

  • There are no DOM events, CustomEvents, postMessage calls or callback options. The player never signals “started”, “answered” or “completed” to the host page. To react to completions or leads, use the server-side hooks.
  • There is no function to mount, re-scan or destroy an instance. The player scans the page once (see below).
  • There is no theming API beyond CSS custom properties.

Markup the player mounts on

The inline shortcode [quizably_quiz id="..."], the block, the popup and the slide-in output this markup (the popup and slide-in place it inside their own wrapper, hidden until opened):

<div class="quizably-quiz-root"
     data-quiz-uuid="36dd2f0a-1a2b-4c3d-8e9f-0a1b2c3d4e5f"
     data-nonce="a1b2c3d4e5"
     data-api-root="https://example.com/wp-json/quizably/v1/public/">
  <noscript>This quiz requires JavaScript.</noscript>
</div>
<script type="application/json" id="quizably-data-36dd2f0a-1a2b-4c3d-8e9f-0a1b2c3d4e5f">{ ...quiz JSON... }</script>
PartMeaning
.quizably-quiz-rootThe mount point. Every matching element on the page is mounted separately, so several quizzes per page work.
data-quiz-uuidThe quiz UUID. Also the key for the hydrated data node.
data-api-rootThe public REST root (.../quizably/v1/public/). Used only as a fallback when QUIZABLY_FRONTEND is missing.
data-nonceA wp_rest nonce. The player does not read or send it. Public routes take no nonce.
script#quizably-data-{uuid}JSON with the same shape as GET /public/quiz/{uuid} (see REST API reference). Encoded with JSON_HEX_TAG and JSON_HEX_AMP, so quiz text cannot close the script tag.
data-quizably-mounted="1"Set on the root after mounting. Guards against mounting twice.
data-auto-start="1"Added by [quizably_quiz autostart="1"] and the block’s Auto-start on load toggle. The player skips the intro screen, unless saved progress is waiting for the visitor’s choice.

Important behaviors:

  • The player reads the quiz only from the hydrated JSON node. It never fetches the quiz from the REST API. A root without a matching quizably-data-{uuid} node mounts with no data and shows an empty shell.
  • The popup and slide-in shortcodes print the same JSON node. If the same quiz appears more than once on a page, for example inline and in a popup, the JSON node is printed only once and every root uses it.
  • Full-page caching serves the hydrated JSON as it was when the page was cached, so quiz edits do not appear until the cache is purged.
  • Rendering the root through AJAX or client-side navigation after load is not supported, because the scan runs once at DOMContentLoaded (or immediately if the document is already past loading).
  • The standalone embed page https://example.com/?quizably_embed=ID wraps the same markup in a .quizably-embed-wrap element. The player detects this wrapper and then allows full viewport height.

When a root mounts, the player immediately calls POST /public/submissions/start, then saves each answer (debounced by 400 ms per question), then calls complete and, if a lead form is used, optin. This happens when the page loads, even for a popup or slide-in that is still closed. On a poll, the result screen also calls GET /public/quiz/{uuid}/poll-results.

If a visitor leaves mid-quiz, the player keeps their place in the browser for 7 days under localStorage (or sessionStorage) key quizably_progress_{quiz uuid} and offers “Resume where you left off?” next time. Names, emails, consent and short text answers are never saved there. Resuming starts a new submission and replays the saved answers into it.

Script and style loading

ItemValue
Script handlequizably-frontend (depends on wp-i18n; the player’s text is translated through wp_set_script_translations())
Style handlequizably-frontend (the file is assets/dist/styles.css)
Script fileassets/dist/frontend.js, loaded in the footer
Script typetype="module". The tag is rewritten through the script_loader_tag filter to <script type="module" src="..." id="quizably-frontend-js">
Version stringFile modification time (filemtime), for cache busting
WhenRegistered on wp_enqueue_scripts, but enqueued only when a quiz renders (the shortcode, block or embed page calls ensure_enqueued()). Pages without a quiz do not load it.

The assets are only registered if assets/dist/frontend.js exists. The bundle needs a browser that supports ES modules. Shared Vue code lives in separate chunk files that the module imports.

Customizing and dequeuing

  • To change the look, override CSS custom properties (below). That is the supported route.
  • To remove the player entirely, for example because you build your own interface on the public REST routes, use the standard WordPress calls wp_dequeue_script( 'quizably-frontend' ) and wp_dequeue_style( 'quizably-frontend' ). Because enqueueing happens while the shortcode renders, run them late, for example on wp_print_footer_scripts at priority 5. This ordering is derived from reading the code and is not tested. Without the script, the placeholders stay empty.

QUIZABLY_FRONTEND

Printed with wp_localize_script() as a global variable before the module runs:

window.QUIZABLY_FRONTEND = {
  apiRoot:   "https://example.com/wp-json/quizably/v1/public/", // or ...?rest_route=/quizably/v1/public/
  pluginUrl: "https://example.com/wp-content/plugins/quizably/",
  version:   "1.2.0"                                            // plugin version
};

apiRoot ends in a slash and keeps the right shape for plain permalinks (the player appends paths and & separated parameters accordingly). The player’s own fetch calls send JSON and no authentication header.

window.Quizably

The player creates or extends window.Quizably:

PropertyTypeDescription
Quizably.versionstringJS API version, currently "1.0.0". A console error is logged if two bundles disagree.
Quizably.vueobjectThe Vue namespace (import * as Vue). Lets extension scripts use the same Vue runtime.
Quizably.frontendHooks.registerTemplate( key, component )functionRegister a template component under key.
Quizably.frontendHooks.registerQuestionType( key, component )functionRegister a question type component under key.
Quizably._templates, Quizably._questionTypesobjectsPlain registries written by the two functions above. Read at render time and not reactive.

Other underscore-prefixed properties on window.Quizably are read internally by the templates and are not a documented API.

A registered component takes precedence over the built-in one for the same key. If a key is unknown, the player falls back to classic for templates and to the single choice component for question types.

Registration order

The registries are plain objects and the player does not re-render when they change. The bundle starts mounting as soon as it evaluates. frontendHooks only exists after the bundle has run, so a normal script that calls registerTemplate() may find nothing to call, or run too late. An order-safe pattern, derived from reading main.js (not run in a browser), is to fill the registry directly from a classic script that is printed in the page before the module executes. Components then use window.Quizably.vue lazily inside their render function, because Vue is not loaded yet when the script runs:

add_action( 'wp_footer', function () {
    ?>
    <script>
    window.Quizably = window.Quizably || {};
    window.Quizably._questionTypes = window.Quizably._questionTypes || {};
    window.Quizably._questionTypes.thumbs = {
      props: ['question', 'value', 'autoAdvance'],
      emits: ['update:value', 'advance'],
      render() {
        const h = window.Quizably.vue.h;
        const self = this;
        return h('div', { class: 'my-thumbs' },
          (this.question.answers || []).map(function (a) {
            return h('button', {
              type: 'button',
              onClick: function () { self.$emit('update:value', a.id); self.$emit('advance'); }
            }, a.label);
          })
        );
      }
    };
    </script>
    <?php
}, 5 );

Priority 5 on wp_footer places the script before the footer scripts (priority 20) where the module tag is printed. This depends on the page printing the footer in the normal way.

Custom question type component

The player renders your component inside the question screen with these props and listeners:

PropTypeNotes
questionobjectPublic question payload: id, type, title, settings, answers[] (id, label, value, media_url, personality_result_id, position). Correctness and points are not included.
valueanyThe current answer held by the player.
autoAdvancebooleanWhether the author enabled auto advance.
EventPayloadEffect
update:valuethe new valueRecords the answer and triggers the debounced save.
advancenoneMoves to the next question (the same as pressing Next). Subject to the required check.

How a value is sent to the server decides what your component should emit. For types in the player’s fixed value list (currently short_text, rating and slider) the value is sent as text_value. For every other type, including custom ones, the player treats an array as answer IDs, a number as one answer ID, and a non-numeric string as text_value. A custom type that emits a plain number is therefore saved as an answer ID, not as text. The scorers only know about answer IDs and the built-in types, so a custom type needs a custom scorer to have any effect on the result (see Hooks and filters).

The question type string comes from the question row. The free builder cannot author custom types, so set type through POST or PUT on the question routes of the REST API.

Custom template component

A template receives:

PropTypeNotes
quizobjectThe hydrated quiz payload (including settings, design, questions, results).
stateobjectThe quiz flow. Refs (read .value): screen, currentIndex, answers, result, score, breakdown, leadData, wasBranched, questions, totalQuestions, currentQuestion, progress, isCurrentAnswered. Functions: start, next, forceNext, prev, setAnswer, submitForm, skipForm, complete, reset.
emitNavfunctionemitNav( action, payload ). Actions: start, next, prev, expired, answer (payload { questionId, value, answer_order }), complete, form (lead form payload), skip, retake.

state.screen.value is one of intro, question, form, result, completed. The built-in screen components (intro, question, form, result) are internal to the bundle and are not exposed on window.Quizably, so a custom template has to render its own screens and call emitNav and the state functions. Plan for this as a substantial piece of work. The template key is chosen through quiz.template. To list it in the Design tab picker, add it with the quizably_templates filter.

CSS custom properties

Quiz.vue sets inline custom properties on the .quizably-quiz element (a child of the root) from the quiz’s saved Design tab values, and the same element carries defaults. You can override them from theme CSS.

PropertyDefault and source
--quizably-quiz-brandBrand color, from design.colors.primary. Default var(--brand).
--quizably-quiz-textText color, from design.colors.text. Default var(--ink-1).
--quizably-quiz-accentAccent color, from design.colors.accent. Default var(--accent).
--quizably-quiz-bgBackground, from design.colors.background and design.background_opacity. Always emitted.
--quizably-quiz-surfaceCard or stage surface. Falls back to --quizably-quiz-bg.
--quizably-font-scaleFont scale, design.font_size percent divided by 100, clamped to 0.7 to 1.5.
--quizably-btn-radiusFrom design.button_style: var(--r-md), var(--r-pill) or 0.
--quizably-card-borderSet to transparent when design.show_border is false or a background image is used.
--quizably-card-max-widthCard width. Default 640px, none in full layout mode.
--quizably-card-min-heightDefault auto.
--quizably-inner-max-widthDefault 100%.
--quizably-split-image-width, --quizably-split-content-align, --quizably-split-height, --quizably-split-max-widthSplit screen layout values.
--quizably-quiz-bg-image, --quizably-quiz-overlay-color, --quizably-quiz-overlay-opacity-fracBackground image URL (or none), overlay color and overlay opacity as a fraction.
--quizably-quiz-outer-padding, --quizably-quiz-base-bgSpacing and fallback fill. Different on the standalone embed page.
--quizably-quiz-text-muted, --quizably-quiz-text-subtleDerived from the text color with color-mix().
--quizably-quiz-option-bg, --quizably-quiz-option-bg-hover, --quizably-quiz-option-border, --quizably-quiz-option-letter-bgAnswer option surfaces, derived from the text color.
--f-sans, --f-displayFont stacks. The Design tab font choice overrides them on the quiz element (the bundled defaults are Geist and Fraunces).

Notes on overriding:

  • Inline styles win over class rules. If the author saved a color in the Design tab, a theme rule such as .quizably-quiz { --quizably-quiz-brand: #0a7; } will lose unless you add !important. The cleaner route is to set the color in the builder.
  • The shared design tokens (--brand, --ink-1, --bg-surface, --border-1, --r-md, --shadow-md and others) are defined on :root by the bundle’s stylesheet, so they exist globally on any page that loads a quiz. Avoid clashing names in your theme.
  • The base styles are scoped to .quizably-quiz and do not reset global element styles. Component styles are scoped by Vue, so overriding internal classes (such as .quizably-question__title) from theme CSS may need higher specificity. Internal class names can change between versions.
  • A saved design.custom_css string is injected into the quiz when present, but the free builder offers no field for it.

Last updated October 4, 2026.