Skip to main content

Branded course and session tiles

The problem​

The booking form is often the first part of your site that looks "different". Parents notice when the fonts, colours and card style change halfway through booking. You want the form to look like the rest of the site while Zooza keeps handling capacity, pricing, trials and payment.

How it looks​

Branded booking widget: illustrated programme tiles with duration, and class tiles with price and availability

Two levels of customisation​

LevelEffortYou control
1. CSS on top of the default tilesMinutesColours, fonts, borders, spacing, grid
2. Your own tile templatesAn hourEverything inside each tile: images, copy, badges, buttons

Both levels keep the widget's own logic: the tile wrapper, selection, collapse and the rest of the booking flow.

Level 1: CSS only​

Little Movers uses a plum and sunshine-yellow palette with rounded cards. Scope your rules to your own placeholder so they never leak into the rest of the page:

<div class="lm-booking"
data-zooza-widget="registration"
data-zooza-id="YOUR_API_KEY"
data-zooza-course-list-display="grid"
data-zooza-course-list-columns="2"></div>
<script async src="https://api.zooza.app/widgets/v1/loader.js"></script>
.lm-booking {
--lm-plum: #5b2a86;
--lm-plum-dark: #431f63;
--lm-sun: #ffc83d;
--lm-line: #e6dcef;
--lm-radius: 18px;
font-family: "Nunito", system-ui, sans-serif;
color: #2d2338;
}

/* Programme (age group) tiles */
.lm-booking .zooza_courses_course {
border: 1.5px solid var(--lm-line);
border-radius: var(--lm-radius);
padding: 1.25rem;
background: #fff;
transition: border-color .15s, box-shadow .15s;
}
.lm-booking .zooza_courses_course:hover {
border-color: var(--lm-plum);
box-shadow: 0 6px 18px rgba(91, 42, 134, .12);
}
.lm-booking .zooza_courses_course.selected {
border-color: var(--lm-plum);
background: #faf6fd;
}

/* Class tiles */
.lm-booking .zooza_schedules_schedule {
border: 1.5px solid var(--lm-line);
border-top: 4px solid var(--lm-sun);
border-radius: var(--lm-radius);
padding: 1rem 1.25rem;
}

/* Select / Choose buttons */
.lm-booking .zooza_courses_course_select,
.lm-booking .zooza_schedules_schedule_select {
border-radius: 999px;
border: 2px solid var(--lm-plum);
background: transparent;
color: var(--lm-plum);
font-weight: 700;
padding: .5rem 1.25rem;
}
.lm-booking .zooza_courses_course_select:hover,
.lm-booking .zooza_schedules_schedule_select:hover {
background: var(--lm-plum);
color: #fff;
}

The selected class is added to the chosen tile when course_list_collapse_on_select is false. For other parts of the form, download the widget's default stylesheet and start from its selectors, as described in Use CSS.

Level 2: your own tile templates​

To change what's inside a tile, register render_course_tile and render_schedule_tile. Your function returns the tile's inner HTML. The widget keeps the wrapper, selection and booking flow.

Little Movers stores two pieces of public metadata on each programme in Zooza: thumbnail (an image URL) and duration (minutes, as an int). The tiles use both:

<div class="lm-booking" data-zooza-widget="registration" data-zooza-id="YOUR_API_KEY"></div>

<script>
( function () {
'use strict';

// Escape anything that comes from data before putting it into HTML
function esc( value ) {
return String( value == null ? '' : value ).replace( /[&<>"']/g, function ( c ) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ c ];
} );
}

function meta( course, key ) {
var entry = ( course.metadata || [] ).find( function ( m ) { return m.key === key; } );
return entry ? entry.value : null;
}

function render_course_tile( course ) {
var thumb = meta( course, 'thumbnail' );
var duration = meta( course, 'duration' );

return '' +
'<article class="lm-course">' +
( thumb ? '<img class="lm-course__thumb" src="' + esc( thumb ) + '" alt="" loading="lazy">' : '' ) +
'<div class="lm-course__body">' +
'<h3 class="lm-course__title">' + esc( course.name ) + '</h3>' +
( duration ? '<p class="lm-course__duration">' + esc( duration ) + ' min per session</p>' : '' ) +
'<p class="lm-course__desc">' + esc( ( course.description || '' ).slice( 0, 160 ) ) + '</p>' +
'<button type="button" class="zooza_courses_course_select lm-button">See times</button>' +
'</div>' +
'</article>';
}

function render_schedule_tile( schedule ) {
return '' +
'<div class="lm-session">' +
'<p class="lm-session__date">' + esc( schedule.get_date_formatted() ) + '</p>' +
'<p class="lm-session__meta">' +
esc( schedule.get_start_formatted() ) + ' · ' + esc( schedule.get_capacity_formatted() ) +
'</p>' +
'<p class="lm-session__price">' + esc( schedule.get_price() ) + '</p>' +
'<button type="button" class="zooza_schedules_schedule_select lm-button">Choose</button>' +
'</div>';
}

window.ZOOZA = window.ZOOZA || {};
window.ZOOZA.callback = window.ZOOZA.callback || {};
window.ZOOZA.callback.render_course_tile = render_course_tile;
window.ZOOZA.callback.render_schedule_tile = render_schedule_tile;

window.ZOOZA.course_list_columns = 3;
window.ZOOZA.translations = Object.assign( window.ZOOZA.translations || {}, {
'registration.step_select_course': 'Pick an age group',
} );
} )();
</script>
<script async src="https://api.zooza.app/widgets/v1/loader.js"></script>
.lm-course { display: flex; flex-direction: column; height: 100%; }
.lm-course__thumb { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 14px 14px 0 0; }
.lm-course__body { padding: 1rem 1.1rem 1.25rem; display: flex; flex-direction: column; gap: .4rem; flex: 1; }
.lm-course__title { margin: 0; font-size: 1.15rem; color: var(--lm-plum); }
.lm-course__duration { margin: 0; font-weight: 700; }
.lm-course__desc { margin: 0; color: #6a5f74; flex: 1; }
.lm-session__date { margin: 0; font-weight: 800; color: var(--lm-plum); }
.lm-session__meta, .lm-session__price { margin: .25rem 0; }
.lm-button { align-self: flex-start; margin-top: .5rem; }

Why it's built this way​

  • The buttons carry the widget's class hooks (zooza_courses_course_select, zooza_schedules_schedule_select). The widget handles the click, so you write no event listeners. Without them, the widget appends its own default button.
  • Only stable fields are used: name, description, metadata on the course and the get_*_formatted() getters on the schedule. Anything else is internal and may change.
  • Data is escaped before it goes into HTML. Programme names and descriptions are written by staff in the Zooza admin, and the page should not trust them as markup.
  • Callbacks are registered on window.ZOOZA before the loader runs. Merging into existing objects (window.ZOOZA.callback || {}) keeps other scripts' settings intact, for example a WordPress plugin's.
  • Registering render_course_tile switches the course list to grid mode automatically.

Variations​

  • Badges from metadata: add age_from/age_to or level metadata and render them as pills. Filter the same keys with metadata_in.
  • Separators between weeks or terms: use schedule_list_rendered to insert headings between groups of classes.
  • Trial-only page: combine with the multi-step trial booking settings.
Build this with your AI agentBrand the Zooza booking widget to match my site