TBX Docs
Customisation

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.ts
src/book/sounds.ts
import 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:

src/book/01_Chapter/020/atoms.tsx
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:

Check the statements
Select true or false for each statement.
  • The Earth is a planet.
    Answer for: The Earth is a planet.
  • Water boils at 0°C at sea level.
    Answer for: 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.

Shared feedback (1)
Select true or false for each statement.
  • The Earth is a planet.
    Answer for: The Earth is a planet.
  • Water boils at 0°C at sea level.
    Answer for: Water boils at 0°C at sea level.
Shared feedback (2)
Select true or false for each statement.
  • JavaScript and Java are the same language.
    Answer for: 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>

On this page