QFlowLearn: QTI 3.0 authoring
A Native TypeScript QTI 3 Player
May 20, 2026 Open Source

A Native TypeScript QTI 3 Player

A strict TypeScript QTI 3 runtime and native Web Component player with XML import, typed item and interaction models, declaration validation, response processing, outcome scoring, saved-state serialization, fixture coverage, and Playwright-tested browser rendering.

As we added support for more QTI 3 question types in QFlowLearn, we realized that we needed a player that supports the full QTI 3 interaction set.

Once the current item interaction set was in scope, we also had to ask what we wanted from a player in the long run. For longsightgroup/qti3, the answer was fairly quick: native Web Components, low dependencies, and strict TypeScript.

React and Vue are wonderful frameworks, but they are heavy for a reference item player, and they wrap implementation choices around their own component model. We wanted the parsing, validation, response processing, rendering, scoring, and saved-state contract to be readable without pulling in a framework runtime.

QTI 3 is a contract

QTI 3 gives assessment content a concrete contract. It covers item structure, response declarations, response processing, shared CSS vocabulary, accessibility metadata, package relationships, and results reporting expectations. When an implementation gets those details right, content can move between systems without being rewritten around one vendor’s player.

Where TypeScript helps

The XML import boundary is where TypeScript matters most. XML arrives as strings: element names, attributes, response identifiers, cardinalities, base types, coordinates. The parser still has to validate those at runtime; TypeScript is not a schema validator. But once the XML is translated into a QTI model, the rest of the code can stop passing anonymous nodes around. A response declaration has a known cardinality and base type. An interaction has a known QTI interaction type. Body content is text, an element, an interaction, feedback, or a printed variable. That gives the renderer, response processor, support matrix, and saved state code the same model to work from.

A small core API

This is the kind of core API we want:

import {
  createItemSession,
  parseQtiXml,
  validateAssessmentItem,
} from "@longsightgroup/qti3-core";

const parsed = parseQtiXml(xml);
if (!parsed.document) throw new Error("Unable to parse item");

const validation = validateAssessmentItem(parsed.document);
const session = createItemSession(parsed.document);

session.respond("RESPONSE", "A");
const result = session.score();

console.log(validation.diagnostics);
console.log(result.outcomes);
console.log(result.state);

Learning from existing work

Amp-up’s Vue 3 QTI player has been a useful reference. We have learned from its rendering behavior, response processing, PNP/session control, catalog events, and the messy places where a clean XML spec turns into a real question on a screen.

Studying it helped clarify where longsightgroup/qti3 could take a different path: framework-neutral packages, core logic that runs without browser DOM APIs, and a native Web Component player. Imports, validation, response processing, scoring, fixtures, and saved-state checks can run in CLIs, CI, Node, or Deno without rendering an item first.

Why now

It also lands at a good time for JavaScript. More packages are ESM first. TypeScript is the default for serious library APIs. Vitest has made fast unit tests feel normal. Playwright has made browser behavior testable without building a separate Selenium-era apparatus. Tools like oxlint and oxfmt help for the same practical reason: fewer slow toolchains, fewer plugins, and fewer decisions that have to be rediscovered by every project.

The browser side should stay just as explicit:

import { defineQtiAssessmentItemPlayer } from "@longsightgroup/qti3-player";

defineQtiAssessmentItemPlayer();

const player = document.querySelector("qti-assessment-item-player");

await player?.loadXml(xml, {
  status: "interacting",
  sessionControl: {
    validateResponses: true,
    showFeedback: true,
  },
});

Keeping the stack small

The dependency list is short: TypeScript, Vite for the manual harness, Vitest, Playwright, axe-core, oxfmt, and oxlint. The core has one runtime dependency today: stax-xml, a small ESM parser that works in Node, Deno, browsers, and edge runtimes. We use it to read XML events, then keep those parser events inside the package and expose QTI model types instead. The core should not need a browser framework, an HTTP client, an alert library, or a drag-and-drop package. If a dependency enters, it should make conformance, security, accessibility, or maintenance measurably better. axe-core is there to catch accessibility regressions early in CI. It is not the whole accessibility story, so the support matrix also needs keyboard, focus, forced-colors, and manual assistive-technology evidence.

What is next

Recent commits have focused on public fixtures, package metadata, release checks, inline gap-match rendering, match rows, select-point and position-object behavior, point coordinate tolerances, and less brittle graphical interaction tests.

Certification is next. Before we submit, the support matrix, fixture suite, browser tests, package inspection, and accessibility checks need to match the claims. We also need more manual accessibility testing and better ergonomics for drag and graphical interactions. The repo is public at github.com/LongsightGroup/qti3.

Related Articles

Sakai 25 Patch Highlights: February 2026
February 27, 2026 Sakai

Sakai 25 Patch Highlights: February 2026

February’s Sakai patches focus on grading edge cases, more dependable quiz behavior, and a round of smaller usability fixes that make the platform feel steadier day to day.

Work with Longsight

Bring us the problems that cross campus systems.

Hosted or on-prem. Security questionnaires. LMS integration. Accessibility at scale. These are familiar conversations for Longsight because we have been in them for twenty years.