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

Put audio files in the book's public/ folder (or any URL the book can load) and play them from the callbacks:

import { YesNo } from '@bside-tech/tbx-ui/components';

function playSound(src: string) {
  void new Audio(src).play();
}

<YesNo
  entries={[
    { text: 'The Earth is a planet.', answer: true },
    { text: 'Water boils at 0°C at sea level.', answer: false },
  ]}
  onSuccess={() => playSound('/sounds/success.mp3')}
  onFailure={() => playSound('/sounds/try-again.mp3')}
/>

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