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
You only need the files in dist/ on your website. Use either the minified files (recommended) or the readable ones.
Quick start
- Upload
dist/doordays.min.cssanddist/doordays.min.jsto your website. - Open
builder/index.htmlin your browser, design your calendar and fill in the doors. - Click Get embed code, enter where you uploaded the files and copy the code.
- 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
- Upload the
distfolder towp-content/uploads/doordays/dist/with FTP or your host's file manager. - 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. - 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.
doordays.min.js so the calendar appears straight away. See examples/wordpress-custom-html.txt.Shopify
- Go to Online Store → Themes → … → Edit code.
- Under Assets, upload
doordays.min.jsanddoordays.min.css. - In the builder, click Get embed code → Shopify and copy the code. It's a complete section file with your settings already inside.
- Under Sections, add a new section named
advent-calendar, replace its contents with the copied code and save. - 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
| Option | Default | Description |
|---|---|---|
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. |
startDate | 1 December this year | Date door 1 opens, as YYYY-MM-DD. |
days | 24 | Number 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. |
shuffle | false | Mix the door order (stays the same for every visit). |
featuredDay | 0 | Door number to show double size (for example 24). 0 for none. |
snow | false | Light snowfall inside the calendar. Off automatically for visitors who prefer reduced motion. |
remember | true | Remember opened doors in the visitor's browser. |
preview | false | Allow ?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. |
locale | browser language | Language 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). |
analytics | false | Send an advent_door_open event to Google Analytics 4 when gtag is on the page. |
labels | English | All 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.
| Field | Description |
|---|---|
day | Door number (1 to days). |
type | "message", "coupon", "link" or "video". |
title | Heading. |
text | Body text. Line breaks (\n) are kept. |
code | Coupon code, shown with a copy button (type coupon). |
video | A YouTube or Vimeo link (type video). The player loads only when the visitor presses play. |
url | Adds a button linking here (any type). |
buttonText | Button label (defaults to the button label). |
newTab | true opens the link in a new tab. |
image | Image shown above the text (or the video's cover image). |
imageAlt | Image description for screen readers. |
cover | Picture 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.
| Label | Default |
|---|---|
door | Day {day} |
doorLocked | Day {day}, opens on {date} (screen readers) |
doorOpen | Day {day}, open now (screen readers) |
doorOpened | Day {day}, already opened (screen readers) |
doorPast | Day {day}, closed (screen readers) |
lockedMessage | Not yet! Day {day} opens on {date}. |
pastMessage | Day {day} has closed. Try today's door! |
beforeStart | The calendar opens on {date}. See you then! |
afterEnd | Thank you for joining us this season. |
today | Today |
close | Close |
copy / copied | Copy code / Copied! |
copyManual | Select the code and copy it. |
button | Shop now |
playVideo | Play video |
loading | Opening your surprise… |
error | Sorry, this door could not be opened. Please try again. |
empty | A 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
startDatea "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
| Class | Meaning |
|---|---|
.adc__door.is-locked | Door whose day hasn't come yet. |
.adc__door.is-open | Door that can be opened. |
.adc__door.is-today | Today's door. |
.adc__door.is-opened | Door this visitor has already opened. |
.adc__door.is-past | Closed past door (when pastDoors is "locked"). |
.adc__cell--featured | The 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.
| Event | When | detail |
|---|---|---|
adc:ready | Calendar drawn | today |
adc:open | A door is opened | day, date |
adc:locked | A locked or closed door is clicked | day, date, status |
adc:close | Door content is closed | – |
adc:copy | A coupon code is copied | day, code |
adc:click | A door's link button is clicked | day, 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:
- Upload the
serverfolder to a PHP 7.4+ host. - In the builder, Export JSON and save it as
server/data/calendar.json(replace the demo file). - Open
server/doors.phpand settime_zoneto your store's time zone. - 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.
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
imageAltto 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).