Overview#
Gather is a website for a local church, built for Riverbend Church, a fictional non-denominational church in Lawrence, Kansas. It has a home page with this Sunday's times and a countdown to the next service, a plan-your-visit page, a sermon archive and sermon pages with an audio player, an events calendar with calendar downloads, a small-group finder and ministries, a giving page, an about page with the staff team, contact and prayer requests, and a 404 page. It is plain HTML with Tailwind CSS v4 and one vanilla JavaScript file, with no build step needed to use it. This guide covers version 1.0.0. See it live at freeframe.dev/templates/gather.
Quick start#
-
Unzip the download and open
index.htmlin a browser. Every page works from the file system. -
Or serve the folder, which is closer to how it runs on a host:
shnpx serve . -
You only need Node.js 24 or newer if you change the CSS classes (see Building and deploying).
Pages#
| File | Purpose | Notable sections and features |
|---|---|---|
index.html |
Home | Hero with river illustration, "This Sunday" card with times and the next-service countdown, three-step "I'm new" path, latest sermon with player, next four events, ministries, scripture and figures band, newsletter sign-up |
visit.html |
Plan your visit | What to expect, service times, kids' check-in steps and rooms, accessibility, SVG parking map, FAQ accordion, plan-your-visit form with live summary |
sermons.html |
Sermon archive | "Watch live" panel with countdown and the latest message, series cards, search and series/speaker/book filters with URL state, sermon cards |
sermon.html |
One sermon (sermon.html?id=room-at-the-table) |
Series cover, audio player, key verse, notes, discussion questions with copy button, speaker card, the rest of the series, related sermons |
events.html |
Events | Category filter, month calendar and list views, selected-day agenda, event dialog with "Add to calendar" (.ics) |
groups.html |
Groups and ministries | Small-group finder (day, area, season of life, childcare), ministries for kids, youth, care and music (#kids, #youth, #care, #music) |
give.html |
Give | Frequency, amount chips and custom amount, fund, cover-the-fee option, live total, "connect your payment provider" note, budget bar, funds, FAQ |
about.html |
About | Story timeline, beliefs (#beliefs), leadership, staff grid with SVG portraits |
contact.html |
Contact and prayer | Office details, urgent pastoral contact, contact form, prayer request form (#prayer) |
404.html |
Not found | Signpost illustration and links back |
Project structure#
gather/
index.html … contact.html, 404.html the pages (header and footer repeated in each)
assets/css/style.css compiled Tailwind CSS (generated, do not edit)
assets/js/main.js all JavaScript: church data, artwork, every page's features
assets/img/favicon.svg browser icon (the Riverbend logo)
assets/img/og-image.svg social image source (1200×630)
assets/img/og-image.png social image used by the pages
src/input.css Tailwind source: brand tokens and component classes
package.json scripts to rebuild the CSS
README.md, DOCS.md, CHANGELOG.mdmain.js is split into numbered sections (listed at the top of the file): 1 church settings and schedule,
2 people, series and sermons, 3 events, 4 groups, giving and visit settings, 5 helpers and dates,
6 artwork, 7 header, menu and countdown, 8 audio player, 9 sermon archive, 10 sermon page, 11 events
calendar, 12 groups finder, 13 plan your visit, 14 giving form, 15 forms, 16 boot. A separate back-to-top
block follows.
Customising#
Brand and colours#
The design tokens are in the @theme block of src/input.css. Every utility such as bg-plum or
text-muted comes from them.
| Token | Default | Used for |
|---|---|---|
--color-paper |
#fbf6ee |
Page background |
--color-shell |
#fffdf9 |
Cards and form fields |
--color-sand |
#f4eadb |
Soft panels, filter bars |
--color-line |
#e5d8c5 |
Hairlines and card rings |
--color-ink |
#2b1b36 |
Text, the footer, dark bands and the audio player |
--color-muted |
#5f5468 |
Secondary text |
--color-plum / --color-plum-dark |
#5b2a6e / #431d52 |
Brand colour, primary buttons, links |
--color-lilac |
#efe5f5 |
Selected states, worship events |
--color-apricot / -light / -dark |
#f6a96b / #fde3cc / #9a4716 |
Warm accent (backgrounds), tint, accent text |
--color-sage / -light / -dark |
#c9dcc0 / #e6efe0 / #33603b |
Hills, care ministry, serve events, success |
--color-river / -light / -dark |
#bcd9e8 / #e3f0f6 / #1f5577 |
River, kids' ministry and events |
--color-rose |
#b3364a |
Errors, "Live now", classes events |
To change the brand colour, edit the tokens and rebuild the CSS:
@theme {
--color-plum: #1f5c7a;
--color-plum-dark: #164459;
}Use apricot only as a background: for text on light backgrounds use apricot-dark, which passes
WCAG AA. The SVG artwork (hero, map, avatars, series covers, logo) uses literal hex values; search the
HTML and main.js for #5b2a6e and #f6a96b to recolour it.
Component classes in src/input.css: .btn-primary, .btn-warm, .btn-outline, .btn-light,
.btn-sm, .card, .card-hover, .eyebrow, .h-display, .h-section, .tag, .chip-dot, .link,
.field, .field-label, .field-error, .option and .pill (radio and checkbox cards that highlight
when checked), .check, .faq (the accordion) and .scrub (the player's slider).
Typography#
Two Google Fonts are loaded by a <link> in each page's <head>: Urbanist for headings
(font-display) and Nunito Sans for body text (font-sans). To swap them, change the family=
parts of the link on every page and the names in src/input.css:
--font-sans: "Nunito Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-display: "Urbanist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;Content#
Page copy is in the HTML files. Everything that is listed, filtered or calculated comes from constants at
the top of assets/js/main.js:
| Constant | What it holds |
|---|---|
CHURCH |
Name, timeZone (IANA name, e.g. America/Chicago), timeZoneLabel, full address for calendar files, liveUrl, uidDomain |
SERVICES |
The weekly schedule: day (0 = Sunday), start ('09:00'), minutes, name |
KEEP_DEMO_CURRENT, DEMO_ANCHOR |
Moves demo dates forward (see Demo dates) |
PEOPLE, STAFF |
Staff and speakers with role, email, bio and avatar; STAFF is the order on the about page |
SERIES |
Sermon series: title, subtitle, art (table, sun, wheat or wave), blurb |
BOOKS |
Labels for the book filter |
SERMONS |
Every sermon (see below) |
EVENT_CATS, EVENTS |
Event categories with their colour classes, and events |
DAYS, AREAS, STAGES, GROUPS |
Small-group filter labels and groups |
GIVING |
Amount chips, default amount, min and max, card fee, funds |
VISIT |
Number of Sundays offered, maximum party size, kids' rooms by age |
Add a sermon by adding an object to SERMONS (order doesn't matter; the archive sorts by date):
{
id: 'a-new-sermon', title: 'A New Sermon', series: 'open-table', part: 6, speaker: 'dana',
date: '2026-10-18', book: 'luke', passage: 'Luke 10:38–42', duration: 2100, audio: '',
summary: 'One or two sentences for cards and the page description.',
verse: { text: 'Quoted text.', ref: 'Luke 10:41 (KJV)' },
notes: [['Heading', 'One short paragraph.'], ['Heading', 'Paragraph.']],
questions: ['First question?', 'Second question?']
}speaker is a key of PEOPLE, series a key of SERIES, book a key of BOOKS, and duration is in
seconds. The demo quotes the King James Version, which is in the public domain; check the licence of any
other translation before quoting it.
Add an event to EVENTS. Times are 24-hour, in the church's time zone; add weekly to repeat it every
week until that date:
{ id: 'choir-night', title: 'Choir Night', cat: 'worship', date: '2026-11-05', weekly: '2026-12-17',
start: '19:00', end: '20:30', place: 'Main auditorium', summary: 'Open rehearsal for anyone who sings.' }Add a group to GROUPS; day, area and stage must be keys of DAYS, AREAS and STAGES, spots: 0
shows "Full: join the waitlist":
{ id: 'lunch-break', name: 'Lunch Break', day: 'wed', time: '12:15', area: 'downtown', stage: 'everyone',
host: 'Ana Ruiz', where: 'Riverbend, Room 210', spots: 6, childcare: false, blurb: 'A 45-minute study over lunch.' }Demo dates#
The demo is written for the week of DEMO_ANCHOR ('2026-10-11'). While KEEP_DEMO_CURRENT is true,
every sermon and event date moves forward by whole weeks so that the latest sermon is always last Sunday and
events are always upcoming. Weekdays never change. Set KEEP_DEMO_CURRENT = false once you enter real
dates, or your dates will be shifted too.
Images and artwork#
There are no photos. All artwork is SVG:
| Artwork | Where |
|---|---|
| Hero landscape (church, river, hills) | Inline in index.html, 600×540 |
| Welcome tent, small-group table, 404 signpost | Inline in visit.html, groups.html, 404.html |
| Parking map | Inline in visit.html, 640×420, with a <title> and <desc> |
| Staff portraits | avatar() in main.js, drawn from each person's avatar settings |
| Series covers | seriesArt() in main.js, 320×200, varied by the sermon's part |
| Logo | Inline in each page's header and footer, and assets/img/favicon.svg |
A portrait's avatar object takes skin, hair, style (short, long, bun, curly, coily,
waves or bald), bg, shirt, and optional glasses and beard. Elements with
data-avatar="dana" are filled with that portrait (add data-decorative to hide it from screen readers).
To use photos instead, replace the generated markup with <img> tags: portraits are square (1:1), series
covers 16:10, and the hero frame about 10:9. Give every image meaningful alt text.
The social image is assets/img/og-image.png (1200×630), rendered from og-image.svg. Edit the SVG, then
export it to PNG with any vector editor or browser screenshot at 1200×630.
Navigation, header and footer#
The header (with the service status bar) and footer are repeated in every HTML file. To add or remove a
page, edit the <nav aria-label="Main"> list, the <nav aria-label="Mobile"> list and the footer links on
every page. Mark the current page with aria-current="page" on its link; the styles highlight it
automatically. The "Give" button in the header also uses aria-current on give.html.
Features#
Service status and countdown#
The bar above the header ([data-service-status]) shows "Next gathering: Sunday at 9:00 am", or "Live
now" with a "Watch" link while a service is running. Elements with data-countdown (on the home page and
the sermons page) show days, hours, minutes and seconds to the next service, and switch to a "Live now"
card with a "Watch live" button linking to CHURCH.liveUrl. [data-this-sunday] elements show the date of
the next Sunday with a service still to come.
Everything is computed by serviceState() from SERVICES in CHURCH.timeZone, so a visitor in another
time zone sees the right countdown for your church. Add a service by adding to SERVICES:
{ day: 3, start: '18:30', minutes: 60, name: 'Midweek Worship' }To preview the live state at any time, add ?live=1 to a page's URL, for example index.html?live=1.
The ticking numbers are hidden from screen readers; a visually hidden sentence ("Starts in 5 days, 18
hours and 53 minutes.") gives the same information without announcing every second.
Audio player#
The player is built by playerMarkup() and initPlayer() (section 8). It appears on sermon.html, on the
home page (latest sermon) and in the "Watch live" panel on sermons.html.
- With a real file: set the sermon's
audioto a URL ('audio/room-at-the-table.mp3'or a full URL from your podcast host). The player then drives a real<audio>element: its duration comes from the file, and speed, seeking and progress use the media element. - Without a file (
audio: '', the demo): playback is simulated over the sermon'sdurationwith a timer, so every control works and a note under the player says so. Remove the note by giving each sermon anaudioURL.
Controls: play/pause, a slider for scrubbing (5-second steps), back and forward 15 seconds, and a speed
button cycling through SPEEDS (1×, 1.25×, 1.5×, 1.75×, 2×, 0.75×). Only one player plays at a time.
Keyboard, while focus is inside the player: Space (when not on a button) or K
play/pause, J and L back and forward 15 seconds, arrow keys on the slider move 5
seconds, Home/End jump to the start or end. The slider has an aria-valuetext such
as "12 minutes 5 seconds of 38 minutes 42 seconds", buttons have labels that change between "Play" and
"Pause", and speed changes are announced through a polite live region.
The listening position is saved (see Saved data); returning to a sermon shows "Resuming at 12:04" and cards in the archive show "Resume at 12:04".
Sermon archive#
initSermons() fills the series, speaker and book selects from the data, and filters on every change.
Search matches every word against the title, passage, summary, series and speaker. The filters are kept in
the URL, so links can be shared: sermons.html?series=open-table&speaker=dana&book=luke&q=table. Series
cards toggle the series filter. The result count is a live region; with no results an empty state offers to
clear the filters.
Sermon page#
sermon.html?id=<id> shows one sermon; an unknown or missing id shows the latest sermon with a notice. The
page title and meta description are updated from the sermon. "Copy questions" copies the title, passage and
numbered questions to the clipboard (it needs https or localhost; elsewhere it says copying failed).
Related sermons are the rest of the series, then other sermons by the same speaker.
Events calendar#
initEvents() (section 11) shows every occurrence of EVENTS (weekly events are expanded). The month view
is a table with one button per day; choosing a day shows its events in the "Selected day" panel, with the
next three events after it. On wide screens each day lists up to two events and a "+n more" button. The
list view shows weekly events once in an "Every week" box, then upcoming events by month.
Category pills filter both views. State is kept in the URL: events.html?view=list&cat=kids,serve&month=2026-11.
Opening an event adds &event=<id>-<date>, so a link opens straight to that event.
The detail is a native <dialog> (focus is trapped and Esc closes it; focus returns to the
button that opened it). "Add to calendar" downloads an .ics file built by buildIcs(), with start and end
times converted from the church's time zone to UTC, so it imports correctly into Google Calendar, Apple
Calendar and Outlook wherever the visitor is.
Groups finder#
initGroups() (section 12) renders filter pills for day, area and season of life from DAYS, AREAS and
STAGES, each with a live count; options that would give no results are disabled. "Childcare provided" is
a checkbox. State is in the URL: groups.html?day=tue,thu&area=west&stage=families&childcare=1. "Ask to
join" links to contact.html?topic=groups&group=<id>, which pre-fills the contact form.
Plan your visit#
initVisit() (section 13) offers the next VISIT.sundays Sundays (computed in the church's time zone;
today is skipped once the last Sunday service has started) and one radio card per Sunday service. Adults and
children use steppers; each child gets an age select, and the summary shows which room they go to from
VISIT.rooms. Children in the last room (youth) count as an extra seat.
Giving#
initGive() (section 14) builds the amount chips from GIVING.amounts and the fund select from
GIVING.funds. Typing a custom amount clears the chips. "Cover the card fee" adds what is needed so that,
after the fee in GIVING.fee (2.2% + $0.30 by default), the church receives the whole gift; the summary
shows the total and what the church receives either way. give.html?fund=pantry pre-selects a fund.
No payment is taken. See Forms for connecting a payment provider.
Scroll reveal and back to top#
Elements with .reveal fade in as they scroll into view; everything above the current element is revealed
too, so jumping to an anchor never leaves gaps. With reduced motion, nothing is hidden or animated. The
back-to-top button is created by the last block of main.js; delete that block and the .back-to-top
styles in src/input.css to remove it.
Forms#
All forms are validated by setupValidation() in section 15 of main.js: errors appear on blur and
submit, the first invalid field is focused, each error is linked with aria-describedby, and a status line
says "Please fix the highlighted fields."
| Attribute | Rule |
|---|---|
required |
Must not be empty (checkboxes must be ticked, selects must have a value) |
type="email", type="tel" |
Must look like an email address / phone number |
minlength |
Minimum length |
data-label |
Name used in the error message |
data-novalidate |
Field is skipped (used by filters) |
| Form | Where | Behaviour |
|---|---|---|
Newsletter (data-form="newsletter") |
index.html |
Replaced with a thank-you message |
Plan your visit (#visit-form) |
visit.html#plan |
Validates contact details and each child's age; confirms the Sunday, time and seats |
Giving (#give-form) |
give.html |
Validates the amount (min and max from GIVING), name and email; explains nothing was charged |
Contact (#contact-form) |
contact.html |
Pre-filled from ?topic= and ?group=; thank-you message |
Prayer (#prayer-form) |
contact.html#prayer |
Request required; email becomes required when "I'd like a pastor to get in touch" is ticked; the message reflects "Keep this private" |
Demo forms don't send anything. To collect submissions, post the form in its callback, for example to Formspree:
setupValidation(contact, () => {
fetch('https://formspree.io/f/your-id', {
method: 'POST', body: new FormData(contact), headers: { Accept: 'application/json' }
}).then((res) => successSwap(contact, res.ok ? '<p>Thanks, we got your message.</p>' : '<p>Something went wrong. Please email us.</p>'));
});Every field has a name, so FormData carries all the values; keep a name on any field you add.
Prayer requests can be sensitive: send them to a service and inbox only your pastoral team can read, and
honour the private field.
Connecting a payment provider#
The giving form never collects card details. To take real gifts, replace the successSwap(...) call in
the setupValidation(form, ...) callback inside initGive() with a request to your server, which creates a
checkout session with your provider (Stripe Checkout, PayPal, or a church platform such as Tithe.ly or
Planning Center Giving) and returns its URL:
fetch('/api/create-gift', { method: 'POST', body: new FormData(form) })
.then((res) => res.json())
.then((data) => { window.location.href = data.url; });Recurring gifts (frequency is weekly or monthly) map to subscriptions in most providers. Many church
platforms also offer an embeddable giving form you can use instead of this one.
Saved data (localStorage)#
| Key | Shape | Used for |
|---|---|---|
riverbend-sermon-progress |
{ "room-at-the-table": 724 } (seconds) |
Resume position for each sermon |
readProgress() keeps only ids that exist in SERMONS and positions between 5 seconds and 5 seconds before
the end; anything else is dropped. A position is removed when a sermon finishes. If storage is blocked, the
player works without saving. To reset, run localStorage.removeItem('riverbend-sermon-progress') in the
browser console, or change PROGRESS_KEY to stop using it.
SEO and sharing#
Each page has its own <title>, meta description and Open Graph tags in its <head>; sermon.html also
updates its title and description from the sermon shown. Social sites need an absolute image URL, so once you
know your domain change every og:image to, for example:
<meta property="og:image" content="https://www.yourchurch.org/assets/img/og-image.png">Add a sitemap.xml listing the pages and a robots.txt pointing to it. For search, consider
Church structured data (a schema.org type) with your address and service times in a
<script type="application/ld+json"> block on the home page.
Accessibility#
- Skip link, landmarks, one
<h1>per page and a logical heading order. aria-current="page"on the current navigation item; the mobile menu usesaria-expanded, closes withEscand when the window grows to desktop width.- Visible focus outlines on every control; radio and checkbox cards show focus on the card.
- The audio player is fully keyboard operable, with labelled buttons and a native range slider with spoken time.
- The calendar is a table with a caption and column headers; day buttons have full-date labels with the
number of events; the event detail is a modal
<dialog>. - Filter results and counts are announced with
role="status"; form errors are linked to their fields. - Illustrations have
<title>text (the map also has a<desc>); decorative SVG is hidden from screen readers. - Colour contrast meets WCAG AA;
prefers-reduced-motionturns off reveals, the live pulse and the river shimmer.
When editing, keep one <h1> per page, give every new input a <label>, and don't use apricot for text.
Building and deploying#
assets/css/style.css is compiled from src/input.css and the classes in the HTML and main.js. Rebuild
it whenever you use a class that isn't already in the template:
npm install
npm run build # once, minified
npm run watch # rebuild on every changeTo publish, upload the whole folder to any static host: Netlify Drop, GitHub Pages, Cloudflare Pages, Vercel or your own server. No server code is needed (except for real payments, see above).
Troubleshooting#
- The countdown is an hour off. Check
CHURCH.timeZoneis your church's IANA time zone (for exampleAmerica/New_York), not a city name or an offset. - My sermons or events have the wrong dates.
KEEP_DEMO_CURRENTis stilltrue, so dates are moved forward. Set it tofalse. - The player says playback is simulated. The sermon's
audiois empty. Add a URL to an audio file; if it is on another domain, that server must allow it to be played (most podcast hosts do). - A new sermon or group doesn't show under a filter. Its
series,speaker,book,day,areaorstagemust be a key of the matching constant. - "Copy questions" fails. The clipboard needs
httpsorlocalhost; it doesn't work from afile://page. - A new class has no effect. Rebuild the CSS (
npm run build). Classes only built in JavaScript must appear literally inmain.jsfor Tailwind to find them. - Fonts look different offline. The fonts come from Google Fonts; without a connection the system fallback is used.
License#
MIT: free for personal and commercial use. See LICENSE.txt in the download. The SVG artwork, logo,
portraits and social image are part of the template and covered by the same license. Scripture quotations
in the demo are from the King James Version, which is in the public domain.