Exercise feedback
Run arbitrary code when an exercise check succeeds or fails
Every exercise accepts onSuccess and onFailure callbacks. Use them to play a sound, show a notification, or run any other function when the student checks their answers.
The shared API is documented under Check callbacks. This page shows customisation examples you can drop into a book page (atoms.tsx or hotspot content).
Play a sound
Import audio files from src so Vite emits hashed URLs that still work after a production build. Do not put clips in public/ and play them with paths like /sounds/success.mp3 — those absolute URLs often fail once the book is built.
Place shared clips in src/_media/_sounds/, import them in a helper, and reuse that helper from any atom.
src/
├── _media/
│ └── _sounds/
│ ├── success.mp3
│ └── try-again.mp3
└── book/
└── sounds.tsimport successSrc from "../_media/_sounds/success.mp3";
import tryAgainSrc from "../_media/_sounds/try-again.mp3";
function playSound(src: string) {
void new Audio(src).play().catch(() => {});
}
export function playSuccessSound() {
playSound(successSrc);
}
export function playTryAgainSound() {
playSound(tryAgainSrc);
}Call the helpers from an atom's onSuccess / onFailure:
import { YesNo } from "@bside-tech/tbx-ui/components";
import { playSuccessSound, playTryAgainSound } from "@/book/sounds";
export function Area1Atom() {
return (
<YesNo
entries={[
{ text: "The Earth is a planet.", answer: true },
{ text: "Water boils at 0°C at sea level.", answer: false },
]}
onSuccess={playSuccessSound}
onFailure={playTryAgainSound}
/>
);
}To apply the same sounds to every exercise in a hotspot, wrap them in <ExerciseFeedback> instead of repeating the props.
If you do not want to ship audio files, a short Web Audio beep works in the browser:
function beep(frequency: number) {
const ctx = new AudioContext();
const oscillator = ctx.createOscillator();
const gain = ctx.createGain();
oscillator.frequency.value = frequency;
oscillator.connect(gain);
gain.connect(ctx.destination);
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15);
oscillator.start();
oscillator.stop(ctx.currentTime + 0.15);
}
<YesNo
entries={entries}
onSuccess={() => beep(880)}
onFailure={() => beep(220)}
/>Browsers may block audio until the student has interacted with the page. Clicking Check counts as that interaction.
With checkOn: "auto", onFailure can fire on every answer change. Prefer onSuccess for sounds in auto mode, or debounce the handler yourself.
Show a notification
Books already mount HeroUI's <ToastProvider /> in src/layout.tsx — the same provider used when the theme changes. Call toast() from the callbacks:
- The Earth is a planet.
- Water boils at 0°C at sea level.
import { toast } from '@heroui/react';
import { YesNo } from '@bside-tech/tbx-ui/components';
<YesNo
entries={[
{ text: 'The Earth is a planet.', answer: true },
{ text: 'Water boils at 0°C at sea level.', answer: false },
]}
onSuccess={() =>
toast('Well done', {
description: 'Every answer is correct.',
variant: 'success',
})
}
onFailure={() =>
toast('Try again', {
description: 'Not quite. Reset and check once more.',
variant: 'danger',
})
}
/>That matches the theme-switcher pattern (toast("Theme changed to", { description, variant: "success" })). Use variant: "success" or "danger" (or toast.success / toast.danger) depending on the result.
Arbitrary functions
The callbacks are plain functions. Call anything you need:
<PickOne
entries={entries}
onSuccess={() => {
window.dispatchEvent(new CustomEvent('tbx:exercise-success'));
console.log('capital question correct');
}}
onFailure={() => {
analytics.track('exercise_failed', { id: 'capital-france' });
}}
/>Page-wide defaults
Wrap a hotspot or page in <ExerciseFeedback> so every nested exercise shares the same handlers. Per-exercise props still override the defaults.
- The Earth is a planet.
- Water boils at 0°C at sea level.
- JavaScript and Java are the same language.
import { toast } from '@heroui/react';
import { ExerciseFeedback, YesNo } from '@bside-tech/tbx-ui/components';
<ExerciseFeedback
onSuccess={() =>
toast('Correct', {
description: 'This exercise is complete.',
variant: 'success',
})
}
onFailure={() =>
toast('Try again', {
description: 'Reset and check once more.',
variant: 'danger',
})
}
>
<YesNo entries={firstEntries} />
<YesNo entries={secondEntries} />
</ExerciseFeedback>