DoorDays

An advent calendar and daily countdown for any website. Each door opens on its own day and reveals a message, coupon code, image, video or link. No jQuery, no frameworks, no account needed.

Introduction

Thank you for purchasing DoorDays. It works on plain HTML sites, WordPress, Shopify, Wix, Squarespace, Webflow and any platform that lets you add HTML. Besides Advent, it runs any daily countdown from 1 to 31 days: the 12 Days of Christmas, Ramadan, Diwali, a product launch week or a back-to-school promotion.

Main features

  • 1 to 31 doors, unlocked by date in the visitor's time zone or a fixed time zone.
  • Door content: messages, coupon codes with a copy button, images, YouTube or Vimeo videos (loaded only on click) and link buttons.
  • Five themes (Classic, Minimal, Playful, Luxury, Corporate), custom colours and door cover images.
  • Pop-up or inline reveal, shuffled doors, a large featured door and optional snowfall.
  • Remembers which doors each visitor has opened.
  • Visual builder that writes the embed code for you.
  • Optional PHP endpoint that keeps door content on your server until its day.
  • Keyboard and screen-reader friendly, right-to-left support, every text editable.
  • Small: about 8 KB of JavaScript and 5 KB of CSS (minified and gzipped).

Files included

doordays/ ├── dist/ ← the files you put on your website │ ├── doordays.min.js (minified script) │ ├── doordays.min.css (minified styles) │ ├── doordays.js (readable script) │ └── doordays.css (readable styles) ├── builder/index.html ← visual builder (open in your browser) ├── examples/ ← working demo pages │ ├── index.html, basic.html, themes.html, custom-days.html, api.html, remote.html │ ├── shopify/advent-calendar.liquid │ └── wordpress-custom-html.txt ├── server/ ← optional PHP endpoint │ ├── doors.php │ └── data/calendar.json ├── src/ ← source code (ES modules and CSS) ├── documentation/ ← this file ├── changelog.txt └── licenses.txt

You only need the files in dist/ on your website. Use either the minified files (recommended) or the readable ones.

Quick start

  1. Upload dist/doordays.min.css and dist/doordays.min.js to your website.
  2. Open builder/index.html in your browser, design your calendar and fill in the doors.
  3. Click Get embed code, enter where you uploaded the files and copy the code.
  4. Paste the code into your page. Done.

To test before December, open the page with ?adc-date=2026-12-10 at the end of the address after turning on Allow test links in the builder (the preview option). Turn it off again before going live so visitors can't peek ahead.

Visual builder

Open builder/index.html directly in Chrome, Edge, Firefox or Safari. Nothing is uploaded anywhere: your draft is saved in your own browser.

  • Calendar: title, first day, number of doors, time zone, what happens to past doors, pop-up or inline reveal, featured door, shuffle and snowfall.
  • Look: pick a theme and, if you like, override its colours.
  • Doors: click a number to edit that door. Doors with content are marked.
  • Text and language: translate or reword every message.
  • Preview as if today is: see the calendar on any date.
  • Export / Import JSON: back up your calendar or move it to another computer. Use the exported file for next year too.

Any website (HTML)

Add the two files once per page (in the <head> or just before the calendar), then the calendar itself:

<link rel="stylesheet" href="/doordays/dist/doordays.min.css">
<script src="/doordays/dist/doordays.min.js" defer></script>

<div data-adc data-adc-config-id="my-calendar">
	<div class="adc-fallback">
		<p><strong>Our Advent Calendar</strong></p>
		<p>Please turn on JavaScript to open today's door.</p>
	</div>
</div>
<script type="application/json" id="my-calendar">
{
	"title": "Our Advent Calendar",
	"startDate": "2026-12-01",
	"days": 24,
	"doors": [
		{ "day": 1, "type": "message", "title": "Welcome!", "text": "See you tomorrow." },
		{ "day": 2, "type": "coupon", "title": "10% off", "code": "JOY10" }
	]
}
</script>

The <script type="application/json"> block holds settings only; browsers never run it as code. The content inside the data-adc element is shown when JavaScript is off, and replaced by the calendar when it's on.

Settings in a separate file

Instead of the JSON block, you can load a file: <div data-adc data-adc-config="/calendar.json"></div>. Files must be served from a web server (not opened as file://).

Several calendars on one page

Give each one its own data-adc-config-id and its own "id" option so their opened doors are remembered separately.

WordPress

  1. Upload the dist folder to wp-content/uploads/doordays/dist/ with FTP or your host's file manager.
  2. In the builder, click Get embed code → WordPress. The code already uses /wp-content/uploads/doordays/dist/; change the address if you uploaded the files somewhere else.
  3. Edit your page, add a Custom HTML block and paste the code. In Elementor, Divi or other page builders, use their HTML or Code element.
Only Administrators and Editors can save scripts in a Custom HTML block. If a performance plugin delays JavaScript, exclude doordays.min.js so the calendar appears straight away. See examples/wordpress-custom-html.txt.

Shopify

  1. Go to Online Store → Themes → … → Edit code.
  2. Under Assets, upload doordays.min.js and doordays.min.css.
  3. In the builder, click Get embed code → Shopify and copy the code. It's a complete section file with your settings already inside.
  4. Under Sections, add a new section named advent-calendar, replace its contents with the copied code and save.
  5. Open Customize and add the DoorDays Advent Calendar section to any page.

To change the calendar later, either paste new settings (builder → Settings (JSON)) into the section's Calendar settings (JSON) box in the theme editor, or replace the section file with a new one from the builder. examples/shopify/advent-calendar.liquid is a general version of the same section, with a box for pasted settings or a link to a settings file.

The calendar refreshes automatically while you edit in the theme editor. Link buttons can point to any product, collection or discount link, for example /discount/JOY10?redirect=/collections/all.

Wix, Squarespace, Webflow and others

Upload the two dist files to a public location (your platform's file manager, or any host). Then paste the HTML embed code from the builder into an Embed / Code / Custom code block. On platforms that put embeds in an iframe (such as Wix's "Embed HTML"), the pop-up opens inside that frame, so give the embed enough height or choose the inline reveal.

Calendar options

OptionDefaultDescription
id""Unique name for this calendar. Keeps each calendar's opened doors separate. Change it each year to start fresh.
title""Heading above the doors.
subtitle""Text under the heading.
startDate1 December this yearDate door 1 opens, as YYYY-MM-DD.
days24Number of doors (1–31). Door n opens n − 1 days after startDate.
timeZone"visitor""visitor" opens doors at midnight in each visitor's own time zone. Use a zone name such as "Europe/London" to open every door at the same moment worldwide.
theme"classic"classic, minimal, playful, luxury or corporate.
colors{}Override theme colours: accent, background, text, door, doorText. Any CSS colour.
reveal"modal""modal" shows content in a pop-up; "inline" shows it below the doors.
pastDoors"open""open" lets visitors open earlier doors later; "locked" closes each door after its day.
shufflefalseMix the door order (stays the same for every visit).
featuredDay0Door number to show double size (for example 24). 0 for none.
snowfalseLight snowfall inside the calendar. Off automatically for visitors who prefer reduced motion.
remembertrueRemember opened doors in the visitor's browser.
previewfalseAllow ?adc-date=YYYY-MM-DD in the address to test any date. Turn off for a live calendar.
previewParam"adc-date"Name of that address parameter.
localebrowser languageLanguage for dates, such as "en-GB", "de" or "ar".
dir"""rtl" for right-to-left languages.
remote""Address of server/doors.php. Door content is then loaded from the server (see Server-protected doors).
analyticsfalseSend an advent_door_open event to Google Analytics 4 when gtag is on the page.
labelsEnglishAll text. See Text and languages.
doors[]Door content. See Door content.

Door content

Each entry in doors describes one door. Every field except day is optional; a door with no content shows the empty message.

FieldDescription
dayDoor number (1 to days).
type"message", "coupon", "link" or "video".
titleHeading.
textBody text. Line breaks (\n) are kept.
codeCoupon code, shown with a copy button (type coupon).
videoA YouTube or Vimeo link (type video). The player loads only when the visitor presses play.
urlAdds a button linking here (any type).
buttonTextButton label (defaults to the button label).
newTabtrue opens the link in a new tab.
imageImage shown above the text (or the video's cover image).
imageAltImage description for screen readers.
coverPicture shown on the closed door itself.

All door text is inserted as plain text, never as HTML, so content can't break your page or run scripts.

"doors": [
	{ "day": 1, "type": "message", "title": "Welcome!", "text": "A surprise every day.\nSee you tomorrow!" },
	{ "day": 2, "type": "coupon", "title": "10% off", "text": "Today only.", "code": "JOY10" },
	{ "day": 3, "type": "link", "title": "Gift guide", "url": "/collections/gifts", "buttonText": "Shop gifts" },
	{ "day": 4, "type": "video", "title": "Movie break", "video": "https://youtu.be/VIDEO_ID" },
	{ "day": 5, "title": "Our recipe", "image": "/images/cookies.jpg", "imageAlt": "Gingerbread cookies" }
]

Text and languages

Change any of these in labels (or in the builder). {day} and {date} are replaced automatically.

LabelDefault
doorDay {day}
doorLockedDay {day}, opens on {date} (screen readers)
doorOpenDay {day}, open now (screen readers)
doorOpenedDay {day}, already opened (screen readers)
doorPastDay {day}, closed (screen readers)
lockedMessageNot yet! Day {day} opens on {date}.
pastMessageDay {day} has closed. Try today's door!
beforeStartThe calendar opens on {date}. See you then!
afterEndThank you for joining us this season.
todayToday
closeClose
copy / copiedCopy code / Copied!
copyManualSelect the code and copy it.
buttonShop now
playVideoPlay video
loadingOpening your surprise…
errorSorry, this door could not be opened. Please try again.
emptyA little surprise is on its way. Check back soon!

Dates are written in the language set by locale (for example "fr" gives "5 décembre"). For Arabic, Hebrew, Persian or Urdu, also set "dir": "rtl".

Dates and time zones

  • Door dates are whole days. Door 1 opens on startDate, door 2 the next day, and so on.
  • Visitor time zone (default): each door opens at midnight where the visitor is. Best for most shops.
  • Fixed time zone: every visitor sees a door open at the same moment, for example midnight in London. Best for prize draws and limited offers.
  • The calendar uses the visitor's device clock. Someone who changes their clock could open a door early; when that matters, use server-protected doors or single-use coupon codes.
  • Before startDate a "calendar opens on…" notice is shown; after the last day, a thank-you notice.

Themes and styling

Choose a theme with theme. Fine-tune it with the colors option or with CSS custom properties in your own stylesheet:

.adc {
	--adc-bg: #102a43;          /* calendar background */
	--adc-text: #ffffff;        /* heading and text */
	--adc-accent: #f0b429;      /* badges, buttons, focus ring */
	--adc-door: #243b53;        /* all doors (overrides theme colours) */
	--adc-door-text: #ffffff;   /* door numbers */
	--adc-surface: #ffffff;     /* pop-up background */
	--adc-radius: 12px;         /* corner radius */
	--adc-gap: 14px;            /* space between doors */
	--adc-door-min: 88px;       /* smallest door size */
	--adc-font: "Your Font", sans-serif;
	--adc-font-display: "Your Heading Font", serif;
}

To style one calendar only, target its element, for example #my-calendar-wrapper .adc. Every class starts with adc, so the calendar never changes the rest of your site, and your site's styles are unlikely to affect it.

Useful classes

ClassMeaning
.adc__door.is-lockedDoor whose day hasn't come yet.
.adc__door.is-openDoor that can be opened.
.adc__door.is-todayToday's door.
.adc__door.is-openedDoor this visitor has already opened.
.adc__door.is-pastClosed past door (when pastDoors is "locked").
.adc__cell--featuredThe large featured door.

JavaScript API

Calendars with data-adc start by themselves. You can also create one from your own code (see examples/api.html):

const calendar = new AdventCalendar('#my-calendar', {
	startDate: '2026-12-01',
	days: 24,
	theme: 'minimal',
	doors: [ { day: 1, title: 'Hello!' } ]
});

calendar.open(5);              // open door 5 if its day has come (returns true/false)
calendar.close();              // close the pop-up
calendar.setDate('2026-12-10');// pretend today is another date ('' = real date)
calendar.update({ theme: 'luxury' }); // change options and redraw
calendar.reset();              // forget opened doors for this visitor
calendar.getState();           // { today, startDate, endDate, doors: [...] }
calendar.destroy();            // remove and restore the original content

AdventCalendar.init();         // start any new [data-adc] elements added later
AdventCalendar.VERSION;        // "1.0.0"

Each element keeps its calendar on element.adventCalendar.

Events and analytics

The calendar element sends these events (they bubble, so you can listen on document). event.detail holds the data shown.

EventWhendetail
adc:readyCalendar drawntoday
adc:openA door is openedday, date
adc:lockedA locked or closed door is clickedday, date, status
adc:closeDoor content is closed–
adc:copyA coupon code is copiedday, code
adc:clickA door's link button is clickedday, url
document.addEventListener('adc:copy', function (event) {
	// Example: send to your own analytics
	console.log('Copied', event.detail.code, 'from door', event.detail.day);
});

With "analytics": true and Google Analytics 4 (gtag.js) on the page, each opened door also sends an advent_door_open event with door_day.

Server-protected doors (optional, PHP)

With the standard setup, all door content is in the page, so a technical visitor could read tomorrow's coupon code in the page source. For most shops that's fine (use single-use or time-limited codes). If it matters, keep the content on your server:

  1. Upload the server folder to a PHP 7.4+ host.
  2. In the builder, Export JSON and save it as server/data/calendar.json (replace the demo file).
  3. Open server/doors.php and set time_zone to your store's time zone.
  4. Remove the door content from your page settings and add "remote": "/server/doors.php" (with your real path). Keep "startDate" and "days" the same as in the JSON file.

Each door is then requested only when opened, and the server answers only on or after its date (in the server's time zone). Fields like cover still come from the page settings.

Keep the data file private. server/data/ includes .htaccess (Apache, LiteSpeed) and web.config (IIS) files that block direct downloads. On Nginx, or for extra safety on any server, move calendar.json outside your public web folder and update doors_file in doors.php. Never leave allow_preview on for a live calendar.

If the calendar is on a different domain than doors.php, set allowed_origin to that domain, for example https://yourstore.com.

Accessibility

  • Doors are real buttons with labels such as "Day 5, opens on 5 December".
  • Arrow keys move between doors; Home and End jump to the first and last door.
  • The pop-up uses the native dialog element: focus moves into it, Escape closes it, and focus returns to the door.
  • Locked-door messages and copy confirmations are announced to screen readers.
  • Animations and snowfall turn off for visitors who prefer reduced motion.
  • Add imageAlt to door images that carry meaning.

Browser support

Current versions of Chrome, Edge, Firefox and Safari on desktop, plus Safari on iOS and Chrome on Android. The pop-up uses the native <dialog> element (Safari 15.4 and newer); older browsers show door content below the calendar instead. Internet Explorer is not supported.

When JavaScript is off, the fallback content inside the data-adc element is shown.

FAQ and troubleshooting

The calendar doesn't appear.

Check that the file addresses in the <link> and <script> tags are correct (open them in a browser tab). If a red "DoorDays:" message appears, it tells you what's wrong with the settings, usually a missing comma or quote in the JSON. Paste your settings into the builder with Import JSON to check them.

All doors are locked.

That's expected before startDate. To test, turn on preview and add ?adc-date=2026-12-05 to the address, or use the date picker in the builder.

How do I start again next year?

Change startDate and give the calendar a new id so returning visitors see all doors closed.

Can I use my own door pictures?

Yes. Set cover on each door, or style .adc__door-front with CSS.

The pop-up appears behind my site's header.

The pop-up uses the browser's top layer, so this shouldn't happen. If your site uses an iframe-based embed (some site builders), choose "reveal": "inline".

Is visitor data collected?

No. The calendar stores only the list of opened door numbers in the visitor's own browser (localStorage). Videos load from YouTube (privacy-enhanced mode) or Vimeo only after the visitor presses play.

Credits and licences

  • All code, styles and SVG illustrations were created by the author for this item. No third-party libraries are included.
  • Fonts: the item uses fonts already on the visitor's device. No font files are included.
  • Demo video (preview only, not included in the download): Big Buck Bunny © Blender Foundation, peach.blender.org, licensed under Creative Commons Attribution 3.0, embedded from YouTube.
  • Demo text and coupon codes are examples only.

See licenses.txt for details. Each Regular License covers one end product (one website or online store). Building calendars for several clients needs one licence per client site.

Changelog

1.0.0 · First release.

Support

If you have a question that this documentation doesn't answer, please contact me through the support form on my CodeCanyon profile page or the item's Comments tab. Include your page address and a short description of the problem, and I'll reply as soon as possible (usually within one business day).