TMTG v1.3.2
Complete Documentation
Options, behavior, and design notes for Tim’s Multi Talented Gallery
1. Quick start
The gallery now launches itself. Include tmtg.js, write the gallery markup, and you're done — no inline <script> call is needed:
<head>
<script src="tmtg.js?v=1.3.2"></script>
</head>
<body>
<div class="tmtgContainer" id="tmtgContainer">
<div class="tmtgGallery" id="tmtgGallery">
<img src="data:," data-src="photo1.jpg" alt="Short description"
data-description="Optional caption">
<img src="data:," data-src="photo2.jpg" alt="Short description">
</div>
</div>
</body>
When the page finishes loading (the DOMContentLoaded event), tmtg.js finds #tmtgContainer, reads its options (if any), and starts the gallery.
Keep tmtg.css in the same folder as tmtg.js. The script loads the stylesheet itself, from its own folder, so the page doesn't link to tmtg.css and can be anywhere on the site: a page at /blog/post.html with <script src="/tmtg/tmtg.js"> gets /tmtg/tmtg.css. (Earlier versions looked for tmtg.css beside the page instead, so a page in another folder lost its styles, or picked up whatever file of that name was there.)
To change how the gallery starts, add an options block (section 2).
Each image's address goes in data-src, so the browser downloads only the few being shown instead of the whole gallery at once. The src="data:," in front of it is a placeholder that downloads nothing: HTML requires every image to have a src, and this one keeps the page valid. Copy it as-is onto every image (see "Large galleries" in section 9). A plain src="photo1.jpg" works too, and is fine for a handful of images. Adding a small thumbnail file to each image with data-thumb lightens the strip too (see "Thumbnail files").
The "?v=1.3.2" after tmtg.js is the version number. When you update to a new version of the gallery, change it to match, so visitors get the new files (see "Updating the gallery" in section 9).
For a live site, use the smaller, minified copies instead: tmtg-1.3.2.min.js, with tmtg-1.3.2.min.css in the same folder. They work exactly the same way (see "Minified files" in section 9).
2. The options block
Options are written as a JSON object inside a <script> element with type="application/json" and class="tmtgOptions". Place it INSIDE the #tmtgContainer div:
<div class="tmtgContainer" id="tmtgContainer">
<script type="application/json" class="tmtgOptions">
{ "start": 3, "thumbs": false }
</script>
<div class="tmtgGallery" id="tmtgGallery">
...
</div>
</div>
Both the type and the class are required. A block missing either one is ignored, and the defaults are used.
The block is optional. With no block, every option uses its default. You only need to list the options you want to change.
3. Option reference
| Option | Type | Default | Description |
|---|---|---|---|
start | whole number | 1 | The slide shown first. Slides are numbered from 1, in the order they appear in the markup. Numbers past the last slide wrap to slide 1; numbers below 1 wrap to the last slide. |
thumbs | true / false | true | Show the thumbnail strip below the gallery when it launches. Visitors can still show or hide it with the thumbnails (film strip) button; their choice stays in effect as they navigate. Fullscreen always opens with the strip hidden, and a narrow gallery (as on a phone) starts with it hidden, unless "toolbar" is false (see section 9). |
captions | true / false | true | Show each image's caption below the gallery when it launches. Visitors can still show or hide it with the caption (info) button; their choice stays in effect as they navigate. The slide number (e.g. "3 / 7") stays visible either way (see "Slide numbers" in section 9). On a phone held sideways, captions start hidden (see "Phones held sideways" in section 9). |
toolbar | true / false | true | Show the toolbar, with its auto-advance, captions, thumbnails, and fullscreen buttons. With false, the image moves up into its space, and captions and thumbnails stay as "captions" and "thumbs" set them. Read "Hiding the toolbar" in section 9 before using it. |
manual | true / false | false | When true, the gallery does NOT launch automatically; you start it yourself with |
autoAdvance | true / false, or a whole number of milliseconds (1000 or more) | false | Start the slideshow advancing on its own as soon as the gallery launches:
A custom interval also applies when visitors turn auto-advance on with the timer button. Visitors whose device is set to reduce motion aren't auto-advanced at launch (see "Reduced motion" in section 9). Decorative galleries default to true instead (see "decorative" below). |
height | a CSS height in quotes, with units, e.g. "85vh" or "600px" | (none) | The height of the whole gallery (image, caption, and thumbnail strip together), e.g. "85vh" or "600px". The image takes whatever height the caption and strip leave. Most pages don't need it: without it, the gallery fills a container that has a height, and otherwise gives the image area a 3:2 shape. See "Sizing" in section 9. |
decorative | true / false | false | Show the images only, as page decoration: no toolbar, arrows, captions, thumbnails, or slide numbers, and no keyboard or swipe navigation. The image fills the gallery inside a thin frame, and the gallery is hidden from screen readers. Overrides "thumbs" and "captions". Auto-advances every 5 seconds unless "autoAdvance" says otherwise; set it to a number for another interval, or false for a single still image. Read "Decorative galleries" in section 9 before using it. |
objectFit | "contain", "cover", or "fill", in quotes | "contain" | How each image fills its frame:
Works in normal, decorative, and fullscreen views. See "Image fit" in section 9. |
pseudoFullscreen | true / false | true | What the fullscreen button does where the browser can't show the gallery fullscreen (iPhones, mainly):
Has no effect on devices with real fullscreen. See "Fullscreen on iPhones" in section 9. |
Types are strict:
- Numbers are written without quotes:
"start": 3(not "3") - Booleans are written without quotes:
"thumbs": false (not "false") - Heights are written with quotes:
"height": "85vh" (not 85vh) - Fit values are quoted and lower case:
"objectFit": "cover"
About auto-advance and visitors
Auto-advance stops as soon as a visitor takes control: clicking the auto-advance (timer) button, the previous/next arrows, or a thumbnail, pressing the left/right arrow keys, or swiping. This gives visitors the ability to pause moving content, as accessibility guidelines (WCAG 2.2.2, "Pause, Stop, Hide") require. Choose an interval long enough for visitors to read your captions — longer captions need longer intervals.
Auto-advance also stops when someone moves into the gallery with the keyboard (Tab), and when keyboard focus lands on a thumbnail. Why? Someone exploring the controls by keyboard, or with a screen reader, shouldn't have the slides changing under them. The thumbnails are redrawn on every advance, so a running slideshow would also keep moving their place in the strip. This follows the standard advice for slideshows (the WAI-ARIA "carousel" pattern). The timer button still restarts it, and it keeps running while they move between the other controls. Clicking with a mouse doesn't count as moving in, so a click on the timer button simply turns auto-advance on or off.
A decorative gallery is the exception: it has no controls, so visitors can't stop it from inside the gallery. The page has to provide a way to stop it instead (see "Decorative galleries" in section 9).
The timer button is a single on/off toggle, like the caption (info) button: it lights up while auto-advance is running and dims when it stops, whether a visitor or tmtgAutoAdvOn() / tmtgAutoAdvOff() started or stopped it.
Why one toggle button instead of separate ON and OFF buttons? Earlier versions swapped two buttons, hiding one and showing the other. That caused two problems for keyboard and screen-reader users:
- The button they had just pressed disappeared, so keyboard focus was lost and they had to hunt for the other button.
- The on/off state was only shown visually, by which button was visible.
The single button stays in place and carries its state in the standard aria-pressed attribute ("true" while running, "false" when stopped). Screen readers announce it as a toggle button, "pressed" or "not pressed", and the stylesheet uses the same attribute to light the button up. One attribute drives both what people see and what assistive technology reports, so the two can't disagree.
The toggle buttons are named for their feature alone: "Auto-advance", "Captions", "Thumbnails", "Full screen". Earlier versions said "Toggle Auto Advance" and so on, but screen readers already announce a toggle button as one, so VoiceOver said "Toggle Captions, toggle button, pressed". Now it says "Captions, toggle button, pressed".
Hovering over a toolbar button brightens it and makes it grow slightly. After a click or tap, it snaps back to its usual size and its color settles over half a second: to white if the toggle is now on, or back to dim if it's off. Moving the pointer off the button and back grows it again. Reaching a button with Tab draws a white ring around it instead, the same ring the thumbnails and strip arrows use.
Why settle after a tap? Phones and tablets keep a tapped button in its hover look until the next tap somewhere else, so the button stayed grown and bright, looking as if it were still being pressed, and a toggle just turned off looked on. Settling shows the press, then shows the button's real state. The size snaps back at once because a slow shrink looked like the button deflating; only the color fades. The arrows do the same (see "Previous/next arrows").
Why a ring for the keyboard? A toggle that's on is already bright white, so brightening can't show that it has focus, and visitors who ask for reduced motion don't get the growing either (see "Reduced motion"). That left only the browser's own focus ring, whose color varies from browser to browser. The white ring is clear on the toolbar's black everywhere. A button that grew inside its ring left the ring almost touching the toolbar's border, so focus shows the ring alone; hovering over a focused button still grows it.
4. Examples
Start on the third slide:
<script type="application/json" class="tmtgOptions">
{ "start": 3 }
</script>
Hide the thumbnail strip:
<script type="application/json" class="tmtgOptions">
{ "thumbs": false }
</script>
Start with captions hidden:
<script type="application/json" class="tmtgOptions">
{ "captions": false }
</script>
Both at once:
<script type="application/json" class="tmtgOptions">
{ "start": 3, "thumbs": false }
</script>
No toolbar, with captions and no thumbnail strip:
<script type="application/json" class="tmtgOptions">
{ "toolbar": false, "thumbs": false }
</script>
Auto-advance every 5 seconds (the default interval):
<script type="application/json" class="tmtgOptions">
{ "autoAdvance": true }
</script>
Auto-advance every 8 seconds, without thumbnails:
<script type="application/json" class="tmtgOptions">
{ "autoAdvance": 8000, "thumbs": false }
</script>
Most pages need no "height": the gallery sizes itself to the width it's given (see "Sizing" in section 9). Set one when your layout needs a fixed size. A height of 85% of the window's height:
<script type="application/json" class="tmtgOptions">
{ "height": "85vh" }
</script>
A fixed height that also shrinks on narrow screens. The browser uses whichever value is smaller: on most screens that's 85% of the window's height, but on a narrow phone held upright it's 120% of the window's width, so wide images aren't lost in a tall black box:
<script type="application/json" class="tmtgOptions">
{ "height": "min(85vh, 120vw)" }
</script>
A decorative banner that fills its container and changes every 5 seconds (auto-advance is on by default in decorative mode). Because a decorative gallery has no controls, the page must give visitors its own way to stop it (see "Decorative galleries" in section 9):
<script type="application/json" class="tmtgOptions">
{ "decorative": true }
</script>
The same banner, changing every 8 seconds:
<script type="application/json" class="tmtgOptions">
{ "decorative": true, "autoAdvance": 8000 }
</script>
A decorative still image (no auto-advance, so no pause button needed):
<script type="application/json" class="tmtgOptions">
{ "decorative": true, "autoAdvance": false }
</script>
The same banner, with each image cropped to fill the frame instead of showing black bands:
<script type="application/json" class="tmtgOptions">
{ "decorative": true, "autoAdvance": 8000, "objectFit": "cover" }
</script>
Launch manually (see section 6):
<script type="application/json" class="tmtgOptions">
{ "manual": true }
</script>
5. Errors, validation, and defaults
A mistake in the options block never stops the gallery from working. Instead, tmtg.js falls back to defaults and explains the problem in the browser's developer console (as a warning beginning with "TMTG:"):
| Problem | Result |
|---|---|
| Block is not valid JSON | All options use defaults. |
| Block is valid JSON but not an object (e.g. a list or a bare number) | All options use defaults. |
| An option has the wrong type (e.g. "start": "3") | That option uses its default; the other options still apply. |
| "autoAdvance" below 1000, or not a whole number (e.g. 500, 2.5, "5000") | Auto-advance stays off at launch; the button keeps its default 5-second interval. A decorative gallery advances every 5 seconds instead. |
| "height" not a quoted CSS height (e.g. 600, "600", "tall") | The gallery fills its container instead (see "Sizing" in section 9). |
| "objectFit" not one of the three values (e.g. "Cover", "none", "stretch") | Images use "contain". |
| "decorative": true with "thumbs": true or "captions": true | The decorative setting wins; thumbnails and captions stay off. |
| An unknown option name (e.g. "thumb" instead of "thumbs") | Ignored (watch for typos, e.g. "thumb" instead of "thumbs"). |
If the gallery isn't behaving as your options describe, open the developer console (F12 in most browsers) and look for TMTG warnings.
6. Launching manually
Set "manual": true when you need to control when the gallery starts — for example, when you add images with your own script after the page loads.
Then call tmtgRun() yourself from an EXTERNAL script file (an inline <script> would be blocked by a strict CSP; see section 8):
my-page.js:
document.addEventListener("DOMContentLoaded", function() {
/* ...add your images to #tmtgGallery here... */
tmtgRun(1);
});
tmtgRun(n) starts the gallery on slide n. The "thumbs" option from the options block still applies, as long as tmtgRun() is called during or after DOMContentLoaded (as above). You can also set it directly with a second argument: tmtgRun(1, 0) hides the thumbnails; tmtgRun(1, 1) shows them. Either way, the setting stays in effect as visitors navigate, and the thumbnails button reflects it. To show or hide the strip without changing slides, call tmtgSetThumbs(true) or tmtgSetThumbs(false).
The "captions" option also applies in manual mode. To show or hide the caption stage yourself, call tmtgSetCaptions(true) or tmtgSetCaptions(false).
In manual mode, the gallery doesn't start auto-advancing by itself, even if "autoAdvance" is set (or the gallery is decorative). A custom interval (e.g. "autoAdvance": 7000) still takes effect for the auto-advance button. To start auto-advancing yourself, call tmtgAutoAdvOn() after tmtgRun():
tmtgRun(1);
tmtgAutoAdvOn();
tmtgAutoAdvOn() always starts the slideshow, because it's treated as a deliberate choice (it's also what the timer button calls). If you call it at launch like this, check the visitor's motion setting first, just as the automatic launch does (see "Reduced motion" in section 9):
tmtgRun(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
tmtgAutoAdvOn();
}
The "decorative" option also applies in manual mode. It can only be set in the options block; there's no function to turn it on or off.
7. Older pages that call tmtgRun() inline
Earlier versions of TMTG required an inline launch after the gallery markup:
<script>tmtgRun(imgIndex);</script>
Pages that still have this line keep working. tmtg.js keeps track of whether the gallery has already been started; if the inline call ran first, the automatic launch is skipped, so the gallery is never started twice.
On a site with a strict Content-Security-Policy, the inline call is blocked — but the automatic launch still starts the gallery, so the page works either way.
Recommendation: remove the inline line and, if you passed a starting slide to it, move that number into the options block as "start".
8. Why JSON? (and why this works with a strict CSP)
The problem with launching inline
Many sites protect themselves from cross-site scripting (XSS) with a Content-Security-Policy (CSP) header such as:
Content-Security-Policy: script-src 'self'
This tells the browser to run JavaScript ONLY from external files on the site's own origin. Every inline script is refused — both onclick="..." style attributes and <script>...</script> blocks written into the HTML.
Under that policy, the old inline launch line was blocked. tmtg.js still loaded, but the gallery was never built: no buttons, no thumbnails, no captions, and all the images stayed stacked on the page.
The site's only workaround was to weaken its CSP (for example by adding 'unsafe-inline', which disables much of its XSS protection everywhere else on the site) or to maintain a hash or nonce for the snippet. Having the gallery launch itself removes that burden: the simplest setup is also the secure one.
Why the options block isn't blocked by CSP
CSP's script-src rule only controls scripts the browser actually RUNS. A <script> element whose type is not a JavaScript type — such as type="application/json" — is treated as a "data block": the browser stores its text but never runs it. CSP therefore ignores it.
tmtg.js reads the block's text with JSON.parse(), which only reads data and never runs code (unlike eval()). The worst a malformed or hostile options block can do is set an unusual option value — and every value is checked before it's used (section 5).
Why JSON instead of data- attributes
Options could also have been written as attributes on the container, e.g. <div id="tmtgContainer" data-start="3" data-thumbs="false">. JSON was chosen because:
- Real types. In JSON, false is a true boolean and 3 is a number. Every data- attribute is a string, and the string "false" counts as TRUE in JavaScript unless every option is carefully converted.
- Room to grow. Nested settings and lists (for example, per-language labels or per-slide settings) fit naturally in JSON; in attributes they quickly become awkward.
- Self-contained galleries. Because the options block lives inside its own container, each gallery can find its own options. This keeps the door open for supporting more than one gallery per page in the future. (Currently, TMTG supports one gallery per page, identified by the fixed IDs
tmtgContainerandtmtgGallery.)
Trusted Types
Some sites add a second rule to their Content-Security-Policy:
Content-Security-Policy: require-trusted-types-for 'script'
Most cross-site scripting attacks work by getting a page's scripts to treat text as HTML, through features such as innerHTML that turn a string into page content. Under this rule (Trusted Types), the browser refuses plain text in those features altogether, so a whole class of attack becomes impossible, even in code the site didn't write itself.
tmtg.js never asks the browser to turn text into HTML. Its controls are built element by element (createElement and setAttribute), and captions are read as plain text, with the four formatting tags built the same way (see "Caption formatting" in section 9). So the gallery works unchanged on a site that enforces Trusted Types, without the site having to approve anything for it.
Earlier versions built their controls from HTML text and parsed captions with innerHTML. Under Trusted Types the browser blocked the first of those steps, and the gallery never appeared. Building elements directly has a second benefit everywhere: an attribute value, such as a translated label with a quotation mark in it, is stored as it is, so it can never end the attribute early or turn into markup.
9. Things to watch for
Supported browsers
The gallery works in full in:
- Chrome and Edge
- 111 or later (March 2023)
- Firefox
- 113 or later (May 2023)
- Safari on a Mac
- 16.2 or later (December 2022)
- iPhone and iPad
- iOS or iPadOS 16.2 or later
Other browsers built on Chrome's engine (Samsung Internet, Opera, Brave, Vivaldi) work from their versions based on Chrome 111. On an iPhone or iPad, the iOS version is what counts, not the browser: nearly every browser there, Chrome and Firefox included, uses Safari's engine.
Within that range, two newer features are used where the browser has them, and skipped where it doesn't, with nothing lost but polish:
- Firefox before 128 (including Firefox ESR 115) doesn't know
@property, which keeps a few spacings exact. The gaps around the arrows and toolbar come out a few pixels different. - iOS before 17 has no popover "top layer", so on iPhones the window-filling fullscreen is an ordinary overlay (see "Fullscreen on iPhones" below).
Older browsers aren't supported. The script still runs and the images still change, but the layout comes apart:
- Before Chrome 105, Safari 16 and Firefox 110, which added container queries, the image fills the whole width at its full height, the caption shrinks to a narrow box, and the thumbnail strip shows a single, cropped thumbnail.
- Before Chrome 111, Safari 16.2 and Firefox 113, which added
color-mix(), the toolbar loses its outline, and the icons of buttons that are off disappear, though the buttons still work.
Internet Explorer can't run the script at all.
Why these versions? The gallery measures its own width with container queries (see "Sizing" below), and there's no good fallback for that: without them, every size in the gallery would have to be worked out from the window again, which is what earlier versions did and got wrong. The newest feature it needs is color-mix(), which makes the see-through outlines and tints from the page's own colors, so that sets the minimum. Browsers have updated themselves automatically for years, so few visitors are still on versions this old. The main exception is older iPhones and iPads that can't install iOS 16: the iPhone 7 and earlier, from 2016 and before.
How it's tested: each release is checked in the current Firefox and Chrome, in WebKitGTK (Safari's engine, on Linux), and on a real iPhone. Edge isn't tested separately, since it's built on Chrome's engine. Support for the older versions listed rests on the features above, which those versions were the first to have.
Strict JSON syntax
JSON is stricter than JavaScript. The following are NOT allowed and will cause the whole block to be ignored (section 5):
- trailing commas:
{ "start": 2, } - comments:
{ "start": 2 /* second slide */ } - single quotes:
{ 'start': 2 } - unquoted option names:
{ start: 2 }
"</script>" inside the block
The browser ends the block at the first "</script>" it sees, even inside a JSON string. None of the current options contain text, so this only matters if a server generates the options block from user-supplied data in the future. In that case, write it as "<\/script>" (a valid JSON escape). Most server-side JSON encoders can do this for you — for example, PHP's json_encode() with the JSON_HEX_TAG flag.
Placement
The options block must be inside #tmtgContainer. A block elsewhere on the page is not read.
When options are read
Options are read once, when the page finishes loading. Editing the block afterward with a script has no effect on a running gallery.
Sizing
The gallery is a single stack: the image on top, then the caption, then the thumbnail strip. The caption and strip take the room they need, and the image scales to fit the rest. How tall the whole stack is depends on where its height comes from:
- The "height" option, e.g.
{ "height": "85vh" }. The gallery is exactly that tall. Hiding the caption or thumbnails gives the image more room; the gallery itself stays the same size, so the page around it doesn't jump. - No "height" option, and the element around the gallery has a height of its own (e.g. a sidebar panel set to 500px, or a grid cell). The gallery fills it, and behaves as in case 1.
- No "height" option, and the element around it has no height of its own — the usual case for a plain
<div>or<main>in the flow of a page. There's nothing to fill, so the image area takes a 3:2 shape (three wide for every two tall) at whatever width the gallery has, but never more than 60% of the window's height (the whole height on a phone held sideways; see "Phones held sideways" below), and the caption and strip stack below it. In a very short window, the 60% limit stops once the image area is 200 pixels tall, and the page scrolls instead (see "Why a 200-pixel floor?" below). Here the gallery gets shorter when visitors hide the caption or thumbnails.
To choose another shape for case 3, set --tmtg-aspect in your own stylesheet, as width / height:
#tmtgContainer { --tmtg-aspect: 16 / 9; } /* screenshots, video */
#tmtgContainer { --tmtg-aspect: 4 / 3; } /* phone photos */
Why 3:2? It's the shape of most camera photos, and it sits between phone photos (4:3) and screen shapes (16:9), so neither gets wide black bands. Earlier versions made the image area about twice as wide as it was tall, which left even ordinary photos with bands down both sides. No shape suits every image: portrait photos always get side bands, and panoramas get them above and below. --tmtg-aspect only matters in case 3; with a set height, the height decides.
Why a 200-pixel floor? The 60% limit is there so the gallery fits on screen, but in a window only a few hundred pixels tall it can't fit anyway: the caption and thumbnail strip keep their size, and the toolbar keeps its space. Without a floor, the image was the only part that gave way, shrinking to nothing in a window 100 pixels tall, while the thumbnails stayed full size. Past that point, scrolling the page is better than losing the picture. 200 pixels keeps a photo clearly larger than its thumbnail, and the floor only starts in windows under about 435 pixels tall, so ordinary windows look the same as before. The floor never stretches the image past its shape: a narrow gallery whose 3:2 shape is already shorter keeps that shape. Like --tmtg-aspect, the floor only matters in case 3.
In a narrow gallery, 576 pixels wide or less, the thumbnail strip starts hidden (visitors can show it with the thumbnails button; see "Thumbnail strip"), and the arrows, toolbar, and slide number shrink with the gallery. That happens on a phone, but also on a large screen when the gallery sits in a narrow column.
The toolbar stops shrinking when its buttons are 24 by 24 pixels, at a gallery width of 360 pixels. That's the smallest touch target the accessibility guidelines allow (WCAG 2.5.8): smaller buttons are hard to hit with a finger, especially for people with tremors or limited dexterity, and a near miss taps the wrong button. The slide number isn't a button, so it can go a little smaller, but stops at 12 pixels so it stays easy to read.
Things to watch for:
- "Fill its container" means 100% of the container's height. If the container also holds other content (a heading, say), the gallery will be taller than the space left; use the "height" option, or give the gallery its own wrapper.
- A percentage height (e.g. "height": "50%") is a share of the container's height, so it only works in case 2.
- You can set the height from your own stylesheet instead of the options block:
#tmtgContainer { --tmtg-height: 85vh; } - The gallery must get its width from the page, as an ordinary
<div>does: it fills the width of whatever it's in. Inside something that shrinks to fit its contents (a float, an inline-block, or a flex item with no width of its own), it would shrink to its smallest size, 200 pixels. Give it a width there, e.g.#tmtgContainer { width: 600px; }
Why the gallery measures itself, not the window: the arrows, the caption text, the narrow layout, and the default height all depend on how wide the gallery is. Earlier versions measured the window instead, which was only right when the gallery was as wide as the window. In a 500-pixel column, resizing the window changed the image's size, though the column never moved; and a gallery in a sidebar kept a thumbnail strip too cramped to use, because the window was wide. The gallery now asks the browser for its own width, using what CSS calls a "container query", so it looks the same wherever it's placed and however big the window is.
The window still counts in two places, on purpose: the default height never goes past 60% of the window's height (all of it on a phone held sideways), so a wide gallery still fits on screen; and fullscreen fills the screen.
The cost is the 200-pixel-wide case above. To measure the gallery's width without going round in circles, the browser has to settle that width before looking inside the gallery, so the width can no longer come from the gallery's contents.
Why a CSS variable (--tmtg-height) instead of the style "height"? A height set directly on the element would also apply in fullscreen and override the full-screen height. The variable is only read by the gallery's normal-view rule, so fullscreen always fills the screen. And because tmtg.js sets it from script, it works under a strict Content Security Policy that blocks inline style="..." attributes (see section 8).
Fullscreen
Fullscreen works like case 1 above, with the screen as the height.
Thumbnails: fullscreen always opens with the strip hidden, whatever the "thumbs" setting, to give the image as much room as possible. Visitors can show it with the thumbnails button. When they leave fullscreen, the strip goes back to how it was before they entered.
Fullscreen on iPhones
iPhones don't let web pages show anything fullscreen except videos; iPads, Android devices, and desktop browsers do. On an iPhone, the fullscreen button makes the gallery fill the browser window instead. It looks and behaves like fullscreen (the same layout, thumbnails hidden on entry, the button lit while it's on, Esc to leave), with one difference: Safari's address bar and toolbar stay on screen. No web page can hide them. In landscape they shrink to a thin strip.
How the gallery decides: it asks the browser whether it can show the gallery fullscreen, rather than guessing from the screen size or the device's name. Earlier versions hid the button on narrow Apple screens, which went wrong three ways: a phone turned sideways is wide, so the button came back but did nothing; an iPad in Split View is narrow, so the button was hidden although fullscreen works there; and so was a narrow Safari window on a Mac. Asking the browser gets every one of these right, and if Apple ever adds fullscreen to the iPhone, the gallery will use it with no update.
The same window-filling mode is used anywhere real fullscreen isn't allowed. That includes a gallery in an <iframe> on another site's page, unless the <iframe> has allow="fullscreen".
To show no button at all in these cases instead:
{ "pseudoFullscreen": false }
What the window-filling mode does, and why:
- The rest of the page is made "inert" while it's on: Tab can't reach it, and screen readers don't read it. Real fullscreen hides the page, so this keeps keyboard and VoiceOver users inside the gallery too, rather than wandering behind something that covers the whole screen. Everything is put back on leaving.
- The page behind is held still, so a swipe on the toolbar or caption doesn't scroll it, and visitors return to the same place on the page when they leave.
- On iOS 17 and later, the gallery is lifted into the browser's "top layer", the same layer real fullscreen uses, as a popover. That keeps it above everything else on the page, whatever your page's own CSS does. On older iPhones it's an ordinary fixed-position overlay, which works on most pages but can be trapped inside an element that has a CSS transform or filter.
For the cleanest result on phones with a notch or Dynamic Island, add viewport-fit=cover to your page's viewport tag:
<meta name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover">
The gallery then extends behind the notch in landscape, and keeps its controls clear of it. Without it, Safari leaves a strip beside the notch in your page's background color. The gallery doesn't change the tag itself, because it affects your whole page.
Phones held sideways
A phone on its side is wide but very short: often only 320 to 390 pixels of height for the whole gallery. With the thumbnail strip, caption, and slide number all below the image, an iPhone SE turned sideways left the image just 42 pixels tall. So on a phone held sideways, the gallery:
- hides the thumbnail strip and its button. Held upright, a phone only starts with the strip hidden, but on its side there's no room for it at all;
- starts with captions hidden. Visitors can turn them on with the caption button, and turning the phone upright again puts the captions back the way they were, the same way fullscreen hides and restores the thumbnail strip;
- moves the slide number's pill up into the toolbar's row, in the top-left corner, made the same height as the toolbar, so it takes no room from the image;
- lets the gallery be as tall as the screen, instead of the usual 60% of it (see "Sizing" above), and never taller. If visitors turn captions on, the image shrinks to make room for them, as in fullscreen, rather than pushing them off the bottom of the screen. This limit applies even with a "height" option: a 600-pixel gallery on a 375-pixel screen could never be seen whole anyway.
Together these give the same iPhone SE a 3:2 photo about 300 pixels tall. With the usual 60% limit, it was about 150.
Why the whole screen height? On a phone on its side, 60% of the height left the photo smaller than a thumbnail on a desktop, with most of the screen black or blank. When visitors scroll to the gallery, it now fills the screen, much as fullscreen does, but without a tap. The cost: as the page first loads, the heading above the gallery pushes its lower part off the screen, so visitors scroll a little to see all of it.
Why "svh" rather than "vh" or "dvh"? All three are shares of the window's height, but on an iPhone, Safari's address bar slides in and out as visitors scroll, and each unit treats it differently. "vh" means the height with the bar hidden, so a gallery 100vh tall would run under the bar while it shows. "dvh" (the "dynamic" height) follows the bar as it moves, so the gallery always fits, but it also resizes in little jumps with every scroll, which is distracting. "svh" (the "small" height) is the height with the bar showing. It never changes while scrolling, so the gallery always fits and holds still. The cost is small: while the bar is hidden, a thin strip of the page shows below the gallery. Browsers too old to know svh (Safari before 15.4, from 2022) skip these rules and keep the usual 60% layout.
In fullscreen on a phone held sideways, the slide number isn't shown at all, for the most image-only view the gallery has. If visitors turn captions on, the number comes back at the start of the caption, so the caption button doubles as the way to check where they are.
Screen readers are unaffected throughout: the number only moves on screen (or is hidden from view only), so they still hear it just before the caption, as on any other screen.
How the gallery tells: it treats a screen as a phone held sideways when the window is wider than it is tall, under 500 pixels tall, and driven by touch. Phones on their side are 320 to 430 pixels tall, and tablets 744 or more, so the 500-pixel line separates them cleanly. The touch test leaves short desktop windows alone. The screen width isn't used, since a phone on its side is as wide as a small laptop.
Hiding the toolbar
With "toolbar": false, the gallery has no toolbar: just the image, the arrows, the caption and slide number, and the thumbnail strip. The image moves up into the toolbar's space, keeping only a small gap from the top edge. When nothing but the slide number is below the image, the gap below the number matches it.
What visitors lose:
- fullscreen, which only the toolbar's button opens;
- the choice to show or hide captions and thumbnails. They stay as the "captions" and "thumbs" options set them, so set those the way you want them;
- the auto-advance button.
Everything else works as before: the arrows, the thumbnails, swipes, pinch-to-zoom, and the keyboard keys (see "Keyboard").
The toolbar is still built, only hidden, so tmtgSetCaptions(), tmtgSetThumbs(), tmtgAutoAdvOn(), and tmtgAutoAdvOff() still work, and your page can offer its own buttons for any of them. The hidden buttons are out of keyboards' and screen readers' reach too: Tab goes straight to the arrows.
A narrow gallery (as on a phone held upright) usually starts with the thumbnail strip hidden, for visitors to show with the thumbnails button (see "Thumbnail strip"). With no toolbar there's no button, so "thumbs" decides at every width instead: with "thumbs": true, the strip shows on phones too. A phone held sideways still hides it.
On a phone held sideways, the slide number moves up to the toolbar's row (see "Phones held sideways"). With no toolbar, the image still leaves that row free, since the number would otherwise cover the corner of a wide image. A lone image has no number, so it moves up there too.
Why think twice before combining it with "autoAdvance"? Accessibility guidelines (WCAG 2.2.2, "Pause, Stop, Hide") ask that visitors can stop moving content. Without the toolbar, auto-advance still stops as soon as a visitor uses the gallery (see "About auto-advance and visitors" in section 3), but no button says so plainly, and nothing starts it again. If the slideshow matters, keep the toolbar, or give the page its own stop button that calls tmtgAutoAdvOff(), as a decorative gallery must.
A decorative gallery has no toolbar anyway, so the option makes no difference there.
Slide numbers
The slide number ("3 / 7") appears at the start of the caption, in a small rounded pill that sets it apart from the caption text. Screen readers still hear "3 / 7:" with a pause before the caption, and a visitor who copies the caption gets just its text, without the number.
The number stays on screen when visitors hide captions: the caption text goes, and the number sits alone in a slim row below the image. Why keep it? Hiding captions is usually about seeing more of the image, not about losing track of where you are in the slideshow; the number is small, and it's the only on-screen sign of how many images are left.
When nothing else is below the image (thumbnails hidden too, as they start in a narrow gallery), the number's row is exactly as tall as the toolbar space above the image, so the image stays centered between the two. With no toolbar, the gap below the number matches the small gap above the image instead. With the thumbnail strip showing, the number sits midway between the image and the strip's frame, with a small, equal gap on each side, so it never touches a tall image.
An image with no caption shows its number in the middle of the caption row, as with captions hidden, rather than alone at the start of an empty line. The row keeps its usual height, so the image stays the same size as visitors move between captioned and uncaptioned images, and the page around the gallery doesn't jump. A caption of only spaces, or only empty tags such as <b> </b>, counts as no caption, since it would show nothing.
If no image in the gallery has a caption, the gallery looks as it does with captions hidden: the number sits in the slim row, and the captions button is left out of the toolbar. With nothing to show, the button would do nothing, and an extra Tab stop that does nothing wastes keyboard users' time. Here the slim row can't make the image jump, since every image has the same row.
On a phone held sideways, the number moves up beside the toolbar instead (see "Phones held sideways" above).
A gallery with only one image shows no slide number at all, since "1 / 1" tells visitors nothing. Its caption, if it has one, appears on its own.
Decorative galleries have no slide number (see "Decorative galleries" below).
Long captions
Outside fullscreen, the caption box is always tall enough for two lines, so the image keeps the same size as visitors move between slides with one- or two-line captions. Longer captions make the box taller, and the image shrinks to make room.
Whenever the gallery has a set height (cases 1 and 2 under "Sizing", and fullscreen), the caption grows to fit its text, up to half the gallery's height. A longer caption scrolls inside its box, by mouse wheel, touch, or (once focused with Tab) the arrow keys, so no text is ever cut off. Why scroll instead of clipping? A caption can carry information that isn't in the image's alt text; if part of it were cut off, some visitors would have no way to read it.
Captions line up on the left, with a ragged right edge. (In a right-to-left language they would line up on the right instead.) Earlier versions justified them, stretching each line to fill the box. On a narrow screen only a few words fit on a line, so justifying opened wide gaps between them, worst with long words, web addresses, and the short first line beside the slide number. Accessibility guidelines (WCAG 1.4.8, "Visual Presentation") advise against justified text for this reason: uneven gaps make reading harder, especially for people with dyslexia.
Caption formatting
A caption (data-description) may use four formatting tags: <i>, <em>, <b>, and <strong>.
<img src="boca.jpg" alt="Two people in a square"
data-description="Dan and Donna visit <i>barrio La Boca</i>.">
Everything else is shown as plain text:
- Any other tag is removed, and its text kept: "
<span>hello</span>" shows "hello". - Attributes are dropped, even on the four allowed tags:
<b class="x">is just bold. <script>and<style>are removed with everything inside them, and so are comments.- A tag left open closes at the end of the caption.
To show a tag itself as text, write its < and > as &lt; and &gt;. The browser turns & into & when it reads the attribute, and the gallery then turns < and > into the characters: so data-description="Use &lt;b&gt; for bold" shows "Use <b> for bold". The gallery understands <, >, &, ", ', , and numbered references such as < at that second step. Other names written that way, such as &eacute;, stay as typed; write é directly, or as é (encoded once), instead.
Why so strict? A caption is put into the page, and HTML in it could do anything a page can: an <img onerror="..."> or a <script> could run code, and a link could lead anywhere. If captions ever come from somewhere you don't fully control, such as a content system or a spreadsheet, a mistake or a malicious entry could otherwise attack your visitors. Allowing only four harmless tags, with no attributes, rules all of that out.
Why the gallery reads captions itself, rather than letting the browser parse them as HTML: some sites turn on a security policy called Trusted Types (see "Trusted Types" in section 8), under which the browser refuses to parse HTML text for a script at all. Reading the caption as plain text and building the four tags itself means the gallery works there too, with nothing for the site to set up.
Large galleries
For more than a handful of images, write each image's address in data-src, with src="data:," in front of it:
<img src="data:," data-src="photo1.jpg" alt="Short description">
The src="data:," is the same on every image, so copy it as-is; only data-src changes from image to image.
The gallery then keeps only five images loaded at a time: the one showing and the two on each side (wrapping around at the ends), the same five the thumbnail strip shows. As visitors move through the gallery, the image coming into range starts downloading and the one leaving range is let go. The neighbors are already loaded, so Next, Previous, and auto-advance feel instant.
Why data-src? A browser starts downloading every <img src> as soon as it reads the page, hidden or not, before tmtg.js has a chance to run. With a plain src, a gallery of 300 photos downloads all 300 up front, and every one the browser draws can take a lot of memory: a 24-megapixel photo uses about 96 MB once decoded, however small the file. data-src means nothing is downloaded until the gallery asks for it.
Why src="data:,"? HTML requires every <img> to have a src, so an image with only data-src fails the W3C validator ("Element img is missing one or more of the following attributes: src, srcset"). Browsers don't mind, and the gallery works either way, but a valid page is easier to check for real mistakes, and some sites, clients, and reviewers require one. "data:," is the shortest valid address there is. It's a "data URL", which carries a file's contents inside the address itself instead of pointing to a server, and here those contents are empty. So the browser has nothing to download, and the gallery swaps in the data-src address when the image's turn comes.
Why not src=""? An empty src fails the validator too ("Must be non-empty"), and some older browsers read an empty address as "this page", downloading the whole page again for every image.
Things to know:
- The placeholder only matters for validation. Images with
data-srcand no src still work, so galleries written without it don't need changing to keep working. - Plain src still works, and small galleries are fine with it. You can mix the two; images with a plain src are simply always loaded.
Without JavaScript,
data-srcimages don't appear at all. Putting the real address in the first image's src (or the "start" one's) in place of the placeholder, e.g.<img src="photo1.jpg" data-src="photo1.jpg" alt="...">means that image always shows, and it can start downloading while the page loads, a moment sooner than
tmtg.jscould ask for it.- A visitor who clicks faster than images download may briefly see an empty slide; the image appears as soon as it arrives.
Thumbnail strip
Each thumbnail shows its slide number in a small dark pill in its bottom-right corner. The current image's number is reversed out, white with dark digits, to mark where you are. The strip shows the current image with two on each side, wrapping round the ends, so at image 1 of 12 it shows 11, 12, 1, 2, 3; the numbers make it clear where the sequence starts over.
The numbers are white on a mostly black pill, so they stay readable over any picture, light or dark. Accessibility guidelines (WCAG 1.4.3) ask for a contrast of at least 4.5:1 for small text; the pill gives about 5.7:1 even over a white image.
The thumbnails are buttons, so they work from the keyboard as well as by mouse or touch:
- The thumbnails are a single Tab stop: Tab lands on the current thumbnail, and the next Tab moves on. The left and right arrow keys move along it, and Enter or Space shows the image a thumbnail stands for. Home, End, Page Up, and Page Down jump further (see "Keyboard" below).
- Keyboard focus follows the current image, staying on the middle thumbnail as the strip redraws, rather than being lost.
- Screen readers announce the strip as "Thumbnails", and each thumbnail as a button, e.g. "Show image 3 of 12", adding "current" to the one being shown. The number in the pill is hidden from them, since the label already says it.
Choosing a thumbnail stops auto-advance, like any other visitor action.
In galleries of more than five images, a small double-arrow button (« and ») at each end of the strip moves five images back or forward: a whole strip's worth, wrapping round the ends as the strip does. On a touch screen where the strip shows, such as a tablet, swiping across the strip does the same: swipe left to go forward, right to go back. Vertical swipes still scroll the page, and a tap on a thumbnail still chooses it.
- The arrows are plain chevrons, dim at rest, like the toolbar icons. The strip's background is always black, so they don't need the dark disc the main arrows use over the image. On hover or keyboard focus they turn white on a rounded patch of the main arrows' dark red, so the two pairs of arrows look related.
- After a click or tap, Page Up or Page Down (with focus in the gallery), or a swipe across the strip, the matching arrow lights up and fades over one second, as the main arrows do (see "Previous/next arrows"). An arrow with keyboard focus stays lit.
- With five images or fewer, the strip already shows them all, so there are no arrows.
- The arrows take a little width from the thumbnails in a narrow gallery, so all five still fit. They're as tall as the thumbnails, but never under 24 pixels.
- Screen readers announce them as "Back five images" and "Forward five images" (in Spanish, "Retroceder cinco imágenes" and "Avanzar cinco imágenes"). With them, Tab goes: back arrow, current thumbnail, forward arrow.
- Pressing either one stops auto-advance, like any visitor action.
Why one Tab stop instead of five? Keyboard users press Tab to move from control to control, and five stops in a row for the thumbnails made getting past the strip slow. One stop, with the arrow keys inside it, is the standard arrangement for a row of related buttons, such as a toolbar or a set of tabs.
The thumbnails shrink in a narrower gallery, so all five always fit, and the strip's frame closes up around them rather than leaving empty space above and below.
In a narrow gallery (576 pixels wide or less, as on a phone held upright), the strip starts hidden, the way fullscreen opens without it. Visitors can show it with the thumbnails button, and if the gallery widens again (a phone turned on its side, say, or a browser window made wider), their earlier setting comes back. On a phone held sideways the strip and its button are hidden altogether (see "Phones held sideways"). With "toolbar": false there's no button to show it, so the strip doesn't start hidden: "thumbs" decides (see "Hiding the toolbar").
When visitors show the strip in a narrow gallery, it has tighter spacing: smaller gaps between the thumbnails, a slimmer frame, narrower « and » arrows, and smaller numbers, so the five thumbnails get most of the width. They're still small: in a gallery of more than five images, about 52 by 29 pixels in a 390-pixel-wide gallery (a typical phone held upright), and 38 by 21 at 320 pixels. The strip arrows never get shorter than 24 pixels, the smallest touch target the accessibility guidelines allow (WCAG 2.5.8). The thumbnails can, at the narrowest sizes, but they're far enough apart that the guidelines' spacing exception covers them: a near miss lands on nothing rather than on the wrong thumbnail.
Why start hidden? On a phone, the strip's room comes out of the image, which is already small, and thumbnails that size show little of each picture; most phone visitors swipe through a gallery instead. Earlier versions hid the strip on phones entirely, button and all. But some visitors would rather see what's coming, so now the choice is theirs.
Thumbnail files
Each image can name a smaller copy of itself for the thumbnail strip, in data-thumb:
<img src="data:," data-src="photo1.jpg"
data-thumb="thumbs/photo1.jpg" alt="Short description">
Without data-thumb, the strip shows the full-size image, shrunk. data-thumb is optional, works with src or data-src, and can be used on some images and not others.
Why bother? The strip shows five images at a time, each about 130 pixels wide. Shrinking a full-size photo to that width doesn't make it any cheaper: the browser still decodes all of it, so five 24-megapixel photos use about 480 MB of memory just to draw five postage stamps. A 320-pixel-wide thumbnail file uses about 0.25 MB. That matters most on phones, which have the least memory to spare.
Things to know:
- Thumbnails follow the same five-image window as
data-src: only the five in the strip are ever loaded, so a gallery of 300 images with 300 thumbnail files still loads just five of them. - Thumbnails load only when the strip is on screen. Where the strip is hidden (as it starts in narrow galleries, and on phones held sideways), no thumbnail files are downloaded; if the strip appears later, say when a phone is turned upright, they load then. While the strip is hidden, the gallery asks the browser to "lazy load" the thumbnails, which means it downloads an image only once it could be seen. While the strip shows, it doesn't: Safari waits a moment before drawing each new lazy image, even one it has already downloaded, and since the strip redraws its thumbnails on every move, they all flashed blank each time.
- The main image is still loaded too, at full size.
data-thumbsaves memory and drawing work; it doesn't replacedata-src. - Make thumbnails about 320 pixels wide, in roughly a 16:9 shape. That stays sharp on high-resolution ("Retina") screens, which draw two or three device pixels for every CSS pixel. The strip crops every thumbnail to 16:9 anyway (see "Image fit"), so a file of another shape loses its edges.
- If a thumbnail file is missing or its address is mistyped, the strip shows the full-size image instead of a broken-image icon, so a mistake costs some memory, not a broken-looking gallery. The browser's developer console lists the failed download once; after that the gallery stops asking for that file until the page is reloaded.
- The address is only ever used as an image source, never as page markup, so a thumbnail address can't run a script.
Decorative galleries
{ "decorative": true } turns the gallery into page decoration, such as a rotating banner at the top of a page. Only the images are shown, inside a thin frame, and the gallery fills its container (see "Sizing" above; the "height" option works too). There is no toolbar, arrows, caption, thumbnail strip, or slide number. To change the frame's thickness, set this in your own stylesheet:
#tmtgContainer { --tmtg-decorative-padding: 1em; }
What the option does for accessibility, and why:
- The whole gallery gets
aria-hidden="true", and each image's alt text is emptied (alt=""). Screen readers skip the gallery, as they would a background image, instead of announcing pictures that carry no information. - Nothing inside the gallery can receive keyboard focus. Content that is hidden from screen readers must never be focusable: a keyboard user would land on something their screen reader says isn't there.
- The left/right arrow keys and swipes are switched off. A normal gallery listens for arrow keys on the page while it's on screen (see "Keyboard" below), and keeps sideways swipes on the image for itself (see "Swipes and pinch-to-zoom"). That's right for a gallery visitors control, but a decorative one would quietly change slides while someone used the arrow keys elsewhere on the page, and a large banner would get in the way of phone users moving around the page.
Only use it when the images really are decoration. If they carry meaning — product photos, event pictures, anything a visitor would miss if it were gone — use a normal gallery. aria-hidden removes the images and their alt text for screen-reader users entirely, so the meaning would be lost to them.
Auto-advance and decorative galleries
A decorative gallery auto-advances by default, every 5 seconds or at the "autoAdvance" interval you set. Why the different default? A decorative gallery has no timer button, so if it didn't advance on its own, nobody could ever see past the first image. To show a single still image instead, set "autoAdvance": false.
An auto-advancing decorative gallery has no button to stop it. WCAG 2.2.2 ("Pause, Stop, Hide") still applies: moving content distracts people even when it carries no information, especially people with attention or vestibular (balance) conditions. So unless you set "autoAdvance": false, your page must give visitors a way to stop it — for example, a button in your own markup that calls tmtgAutoAdvOff() from an external script:
<button id="stopBanner" type="button">Pause banner</button>
my-page.js:
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("stopBanner")
.addEventListener("click", tmtgAutoAdvOff);
});
Put the button outside #tmtgContainer: anything inside it is hidden from screen readers. (A Play button calling tmtgAutoAdvOn() works the same way.)
Why doesn't the gallery provide this button itself? A decorative gallery is meant to be free of controls, so the choice of where and how to offer a pause button is left to the page.
Image fit
The gallery's image frame rarely has the same shape as every image: a wide banner holding a tall portrait, say. "objectFit" decides what happens to the difference:
- "contain" (default)
- The whole image always shows, as large as fits. Black bands fill the rest of the frame.
- "cover"
- The image fills the frame, keeping its shape. Whatever overhangs is cropped, equally from both sides, so the center of the image stays.
- "fill"
- The image fills the frame, stretched or squashed to its shape. Nothing is cropped, but people and objects look distorted.
Why "contain" is the default: it's the only choice that never hides or distorts part of an image. In a normal gallery, the image is the content — cropping off someone's face or the punchline of a comic defeats the purpose. "cover" suits decorative banners and galleries of similar landscape photos, where a full frame matters more than the edges. "fill" is mainly for images that are already very close to the frame's shape, or abstract textures where stretching won't show.
Things to watch for with "cover":
- The frame's shape changes with the screen. A crop that looks fine on a desktop can lose most of a wide image on a phone held upright, and fullscreen gives yet another shape.
- Keep anything important near the middle of each image.
- Alt text and captions should still describe the whole image, not just the part that shows, since the crop varies by screen.
Only these three values are accepted. CSS also has "none" and "scale-down", but in a frame that changes size they show large images cropped at their natural size, or leave small ones unscaled in a large black space.
The thumbnail strip is not affected: thumbnails are always 16:9 and use "cover", so the strip stays even at any gallery width, and its height never changes the main image's size. The crop is set to 20% vertically, primarily to preserve faces in portrait photographs.
Slide transitions
When visitors move through the gallery, the new image slides a short way in from the side it comes from while fading in, settling into place:
- Next, the right arrow key, a swipe to the left, and auto-advance bring it in from the right; Previous, the left arrow key, and a swipe to the right bring it in from the left.
- Next from the last image (wrapping round to the first) still comes in from the right: the direction follows what the visitor did, not the image numbers.
- A thumbnail brings its image in from its own side of the strip.
The first image fades in, as do changes that aren't navigation, such as a page's own tmtgRun(n) call. Showing or hiding thumbnails or captions, or entering fullscreen, doesn't animate the image at all.
Why a short slide, rather than the old image sliding away as the new one arrives? That "push" style looks great, but it needs a temporary copy of the outgoing image, moved by hand, and it misbehaves in ordinary situations: a long caption changes the image's height mid-push, a quick second click cuts it off, and an image still downloading slides in blank. The short slide gives most of the sense of direction with none of those problems, and with less movement on screen. A push may come later, once browsers' "View Transitions" feature, which handles the outgoing image automatically, is widespread and settled.
Decorative galleries keep the plain fade, which is calmer for something meant to stay in the background.
Visitors who ask their device for reduced motion get the fade without the slide (see "Reduced motion" below). A fade involves no movement, which is what makes motion hard on some people, so it's kept as a gentle sign that the image has changed. Apple's own Reduce Motion setting works the same way, swapping slides for cross-fades.
The thumbnail strip moves with the image. Whenever the image moves (Next, Previous, a swipe, auto-advance, a thumbnail, the strip arrows, or Page Up and Page Down), the strip slides over by the same number of places, with the same timing as the image. The thumbnails at one end slide out under the edge of the frame as new ones slide in at the other, so visitors can see where everything went. A jump of more than five places (Home or End, in a large gallery) fades the new thumbnails in instead, since none of the old ones would still be in the strip. With reduced motion, the strip fades too.
Why does the strip slide, when it used to jump? Before, the strip simply redrew itself in the new positions, so a thumbnail that was on the right was suddenly in the middle. The slide shows the movement, which is easier to follow, and it matches the image's own slide.
How it stays light: the outgoing thumbnails are kept for the third of a second they take to slide away, then removed, so the strip never holds more than five extra thumbnails, and only while they're moving. That keeps the five-image window (see "Large galleries") intact. In a test in Chrome, keeping them used no extra memory at all, because the browser was still holding those images anyway. Only the thumbnails' position changes during the slide, which browsers can animate smoothly without redoing the page's layout. A second move before the slide ends carries on from wherever the strip is, rather than jumping back to start over. While the strip is hidden (narrow galleries, phones held sideways), nothing animates and nothing is measured.
Reduced motion
Most devices let people ask for less motion on screen: "Reduce motion" on Apple devices, "Remove animations" on Android, or turning off "Animation effects" on Windows. Websites can read this setting as prefers-reduced-motion. When it's on, the gallery:
- does not start auto-advancing at launch, even if "autoAdvance" is set, or the gallery is decorative. In a normal gallery, visitors can still turn auto-advance on with the timer button, and their choice is respected. A decorative gallery stays on its first slide, unless your page offers its own Play button (see above).
- fades each new image in without sliding it, and fades the thumbnail strip's new thumbnails in instead of sliding the strip (see "Slide transitions" above).
- stops the toolbar buttons from growing when hovered or tapped. They still brighten, so it's still clear which one the pointer is on, and keyboard focus still shows its white ring. (The previous/next arrows never grow; their disc only darkens.)
Why respect it? People turn this setting on because motion makes them unwell or makes it hard to concentrate. Starting a slideshow automatically is exactly the kind of motion they've asked to avoid. The timer button still works, because a visitor choosing to start the slideshow is different from the page starting it for them.
Font
Captions and slide numbers use the device's own interface font (system-ui): Segoe UI on Windows, San Francisco on Apple devices, Roboto on Android. It looks native everywhere and needs no download. The rule applies only inside the gallery, so your page's text is unaffected. To use your site's font instead:
#tmtgContainer { --tmtg-font: Georgia, serif; }
The gallery also sets its own line spacing, so captions are the same height whichever font is used.
The previous/next arrows and the toolbar icons are inline SVG images, not font characters, so they look the same on every device and the font setting doesn't change them.
Swipes and pinch-to-zoom
On touch screens, a swipe sideways across the image moves to the next or previous image: swipe left to go forward, right to go back. The rest of the image's touches belong to the browser:
- Swiping up or down scrolls the page, as it does anywhere else. A swipe has to be fairly steep to count: within about 30 degrees of straight up or down. Anything flatter is treated as a sideways swipe, and the page holds still.
- Pinching zooms in, so visitors can take a closer look at the photo, or at the whole page.
- While zoomed in, dragging with one finger moves around the enlarged view instead of changing the image. Pinch back out to full size, and swipes change images again.
In fullscreen there's nothing to scroll, so swiping up or down does nothing there. Pinching and zooming work the same as on the page, except on an iPad: Safari doesn't allow zooming in real fullscreen on any page, and no page can change that. Leaving fullscreen brings zooming back, and in fullscreen the image is already as large as the screen allows. iPhones use the gallery's window-filling mode instead of real fullscreen (see "Fullscreen on iPhones"), which is an ordinary page, so zooming works there.
Why let the browser zoom? Earlier versions blocked every touch that started on the image, so that the gallery alone decided what each touch meant. That also blocked pinching, so visitors who needed a closer look couldn't zoom in from the image, and in fullscreen on an iPhone, where the gallery covers the whole window, they couldn't zoom at all. Being able to zoom is an accessibility requirement (WCAG 1.4.4), and many visitors with low vision rely on it.
How it works: rather than catching touches in the script, the gallery tells the browser in advance which ones to keep, with the CSS "touch-action" property. On the image it's "pan-y pinch-zoom": scrolling up and down, and pinching, are the browser's; anything else (a sideways swipe) is left to the gallery. While the page is zoomed in, the gallery switches the image to the browser's usual handling, so a one-finger drag moves the view. The zoom is the browser's own, the same as pinching anywhere else on the page, so it enlarges the controls along with the photo. A zoom of just the photo, as in a phone's photo app, may come in a later version.
Why decide the direction in the script as well? Browsers read "pan-y" differently. Chrome looks at which way a touch mostly goes, so a sideways swipe never scrolls the page. Safari scrolls by whatever up-or-down part a touch has, so a sideways swipe that drifted a little also nudged the page up or down, which was distracting. The gallery now looks at the first few pixels of each one-finger touch and decides: steep means scroll, anything flatter means swipe, and a swipe is kept from scrolling the page for the rest of that touch. It works the same in every browser. Pinches are never held back, nor drags while zoomed in, nor anything in fullscreen. The browser does have to wait for that check before it can scroll from the image, but the check is a couple of subtractions, so the wait is too short to notice.
The gallery also measures a swipe by how far the finger moved on the screen, not on the page. Measured on the page, any scrolling during the touch was added to the finger's movement, so a swipe could look longer or more slanted than it really was.
On computers without a touch screen, nothing here applies, and touch screens on laptops keep the browser's usual handling, since the gallery only watches for swipes on phones and tablets.
Keyboard
While the gallery is on screen, the left and right arrow keys move to the previous and next image when keyboard focus is in the gallery, when the visitor last clicked or tapped in it, or when nothing in particular has focus (for example, after a click on the page's text). They're left alone when they mean something else:
- when focus is on anything else on the page: a link, a button, a video, a box that scrolls sideways, or your own menu, tabs or carousel;
- in a text box, text area, drop-down list, slider, or any other form field, where they move the cursor or change the choice;
- with Ctrl, Alt, Shift, or the Command key held, since those make different shortcuts (Alt+Left is Back in most browsers, and Shift+arrows select text);
- when another part of your page has already used the key;
- when the page itself scrolls sideways, unless focus is in the gallery or the visitor last clicked in it.
In fullscreen, where the page is out of sight, they always move the gallery. When the gallery does use the key, the page doesn't also scroll.
Why so many exceptions? Arrow keys normally act on whatever has focus, and keyboard and screen-reader users rely on that. Earlier versions took Left and Right from anywhere on the page, which went wrong in three ways:
- Double actions. The browser's own use of the key still happened, so on a page wide enough to scroll sideways, every press changed the slide and scrolled the page too. A video skipped ahead five seconds, and a page's own tabs or carousel moved, at the same time as the gallery.
- Typing. Someone moving the cursor in a search box beside the gallery changed slides every time. (That check was always there.)
- Zoom. Visitors with low vision often zoom to 200% or more, and many pages then scroll sideways. Those visitors move along each line with Left and Right, and the gallery changed slides with every press while it was on screen. So on a page that scrolls sideways, the arrows belong to the page until the visitor clicks in the gallery or tabs into it.
A click on the image counts because it doesn't move keyboard focus: without that rule, a mouse user who clicked the gallery on a wide page would find the arrows scrolling the page instead.
When keyboard focus is inside the gallery (on a button or a thumbnail), four more keys help with large galleries:
- Home
- the first image
- End
- the last image
- Page Up
- five images back
- Page Down
- five images on
Page Up and Page Down wrap round the ends, like the thumbnail strip and its arrows, so every "move five" control behaves the same. Elsewhere on the page, these keys scroll the page as usual.
Screen reader announcements
When the image changes, a screen reader would normally say nothing, because focus stays on the button that was pressed. So the gallery announces each new image, e.g. "Image 3 of 12. Two people posing in a public square. Dan and Donna visit barrio La Boca." That's the image number, its alt text, and its caption, in the page's language.
The announcement is spoken politely, after whatever the screen reader is already saying. It isn't made when the page first loads, when the gallery redraws the same image (for example when thumbnails are shown or hidden), or while auto-advance is running: a new announcement every few seconds would drown out everything else on the page. This follows the WAI-ARIA "carousel" pattern.
Previous/next arrows
Each arrow is a white chevron on a dark, see-through disc with a faint white ring. Hovering over it, or reaching it with Tab, lights a dark red semicircle behind it, darkens the disc, and turns the chevron fully white.
After a click or tap, that highlight fades away over one second. Moving the pointer off the arrow and back lights it up again.
The Left and Right arrow keys, and swipes on the image, light up the matching arrow the same way, and it fades over one second. Holding a key down keeps the arrow lit until the key is released.
An arrow with keyboard focus (reached with Tab) never fades. It stays lit when pressed with Enter or Space, or with the matching arrow key, for as long as it has focus.
Why fade? Without it, the highlight stayed lit for as long as the pointer rested on the arrow, a red semicircle over the new image. On phones and tablets it was worse: browsers keep a tapped button in its hover look until the next tap somewhere else. The fade confirms the press, then gets out of the way of the picture.
Why light the arrows for keys and swipes? It ties each action to the button it stands for, so visitors learn that the keys and swipes do the same as the arrows. It's also the only sign of direction for visitors who ask for reduced motion, since their new image fades in without sliding (see "Slide transitions").
Why does a focused arrow stay lit? For keyboard users, the highlight is the focus indicator: it shows which button Enter will press. Accessibility guidelines (WCAG 2.4.7, "Focus Visible") require that this stays visible, so it does.
The fade is kept for visitors who ask for reduced motion, since a fade involves no movement.
Why a dark disc? The arrows sit over the image itself in fullscreen, on a phone held sideways, and with "objectFit": "cover". Earlier versions used a faint white disc and a half-transparent white chevron, which disappeared over light images: white on white. Phones also dimmed the arrows further, which made them hard to see even over black. A dark disc works on both: over a white image the disc turns mid-gray, and the chevron still stands out clearly against it; over black, the chevron and the faint ring show the arrow's shape.
Accessibility guidelines (WCAG 1.4.11, "Non-text Contrast") ask that controls stand out from their surroundings by a contrast ratio of at least 3:1. On a plain white image, the hardest case, the chevron stands out from the disc by about 3.4:1, and the disc from the image by about 4:1, at every screen size. The disc was darker at first, but it stood out more than needed over images in fullscreen; it's now lighter, while keeping a comfortable margin above 3:1.
A gallery with only one image has nowhere to go, so it has no arrows or auto-advance button, just as it has no thumbnail strip, thumbnails button, or slide number. "autoAdvance" and tmtgAutoAdvOn() do nothing there either, so no timer runs in the background. Earlier versions showed arrows and a timer button that did nothing, which looked like a gallery that wasn't working, and gave keyboard users extra Tab stops for nothing. If a page adds more images and calls tmtgRun() again, the arrows and the button come back (start auto-advance then with tmtgAutoAdvOn(), if you want it).
Updating the gallery
Browsers keep copies of the files they download, so a returning visitor's browser may reuse an old tmtg.js or tmtg.css instead of asking for the new one. After an update, that visitor could see the old gallery, or worse, the new script with the old stylesheet, until the browser decides its copy is too old.
A version number on the end of a file's address fixes this:
<script src="tmtg.js?v=1.3.2"></script>
To the browser, tmtg.js?v=1.4 is a different address from tmtg.js?v=1.3.2, so it can't have a copy of it and downloads it fresh. The server ignores everything after the "?" and sends the same file.
When you update the gallery, change the number in your page's <script> tag to the new version. tmtg.js adds its own version number to the stylesheet's address when it loads tmtg.css, so the stylesheet takes care of itself.
The minified files carry the version in their names instead, so they need no "?v=" (see "Minified files" below).
Minified files
Each release comes in two forms:
tmtg.jsandtmtg.css, the readable source, full of comments that explain what each part does and why.tmtg-1.3.2.min.jsandtmtg-1.3.2.min.css, the same code with the comments and spare spaces removed, and the script's internal names shortened. The number is the version.
For a live site, use the minified pair. Keep both files in the same folder, as with tmtg.css: the minified script loads the minified stylesheet itself, from its own folder.
<script src="tmtg-1.3.2.min.js"></script>
Everything else is the same: the markup, the options block, and the public functions such as tmtgRun() and tmtgSetCaptions(), which keep their names.
Why bother? The comments make up more than half of each file. Web servers usually compress files on the way to the browser, which narrows the gap but doesn't close it: visitors download about 13 KB for the minified pair, against about 33 KB for the readable files. That's a faster gallery, especially on a slow phone connection.
Why keep the readable files? Minified code is close to unreadable. Use tmtg.js and tmtg.css to learn how the gallery works, to change it, or to track down a problem: a browser's developer tools show the original lines and comments, rather than one long line of code.
Why put the version in the name? A new release is then a new file, which no browser can have kept an old copy of, so the "?v=" trick from "Updating the gallery" isn't needed. When you update, change the name in your page's <script> tag, and replace both minified files.
The minified files are made from the readable ones with the build script, build-min.py, before each release. The script uses terser for the JavaScript, and a small minifier of its own for the CSS that only removes comments and spaces. Each release is tested to look and behave exactly like the readable files, including with a strict Content Security Policy and Trusted Types (section 8).
Language
The gallery's controls take their language from the lang attribute on the hosting page's <html> element:
<html lang="es">- Spanish
<html lang="es-MX">- Spanish (Mexican Spanish uses the general Spanish labels)
<html lang="en-US">- English
- anything else, or none
- English (the fallback)
Upper or lower case doesn't matter: "ES" works the same as "es".
The gallery looks for labels matching the whole code first, then shorter and shorter versions of it: for "zh-Hant-TW" it tries "zh-hant-tw", then "zh-hant", then "zh". That lets a language have regional versions where the wording really differs (European Portuguese as "pt-pt", alongside the general, Brazilian "pt"), while every other region uses the general labels.
A few region codes are special cases, because the region decides something the language code alone doesn't:
- Chinese: the region chooses the writing system. "zh-CN" and "zh-SG" lead to Simplified characters ("zh-hans"), and "zh-TW", "zh-HK", and "zh-MO" to Traditional ("zh-hant").
- Portuguese: Angola, Mozambique, Cape Verde, Guinea-Bissau, and São Tomé and Príncipe ("pt-AO", "pt-MZ", "pt-CV", "pt-GW", "pt-ST") follow European rather than Brazilian conventions, so they lead to "pt-pt".
This sets the text of the toolbar buttons, the previous/next arrows, and the thumbnails and their arrows: the tooltips you see on hover and the labels screen readers announce (e.g. "Auto-advance" / "Avance automático", or "Show image 3 of 12" / "Mostrar imagen 3 de 12"). Your own alt text and captions are shown exactly as you wrote them.
Supported languages:
en- English (the fallback)
es- Spanish
pt- Portuguese (Brazilian wording where the two differ, e.g. "tela cheia" for full screen)
pt-pt- Portuguese, European (e.g. "ecrã inteiro"; also used in Portuguese-speaking Africa)
fr- French
de- German
it- Italian
zh-hans- Chinese, Simplified (also used for plain "zh", and for "zh-CN" and "zh-SG")
zh-hant- Chinese, Traditional (for "zh-Hant", "zh-TW", "zh-HK", and "zh-MO")
ja- Japanese
ko- Korean
hi- Hindi
The French, German, Italian, Portuguese, and Spanish labels follow the wording common in those languages' own software. The Chinese, Japanese, Korean, and Hindi labels are correct and natural, but interface wording has conventions of its own (how formal to be, the usual word for "thumbnail"), so a native speaker's check would be worthwhile before relying on them. These labels are what screen-reader users hear.
Why Traditional Chinese has its own labels instead of the Simplified ones converted character by character: the two are also used in places with different software vocabularies. The Traditional labels follow Taiwan's usual interface words, such as "縮圖" for thumbnail and "全螢幕" for full screen, where mainland software says "缩略图" and "全屏". Hong Kong and Macau share them; their own preferences are close enough that a separate block isn't needed. Before these labels existed, Traditional codes fell back to English rather than to Simplified, which many Traditional readers would find out of place.
Why "pt-pt" repeats the labels it shares with "pt": only two of them differ ("ecrã inteiro" for full screen, "recuar" for going back five images). But a label a block leaves out falls back to English, not to the shorter "pt" block, so a block with only the two differences would show the rest in English. Keeping every block complete means each language works the same way, and a change to the Brazilian wording never slips into the European labels unnoticed.
To add a language, open tmtg.js and find tmtgLabels near the top. Copy the English block, translate each label, and name the copy with the language's code in lower case, e.g. 'nl' for Dutch. In labels with {n} and {total}, the gallery fills in the numbers, so put them wherever the language's grammar wants them: Japanese, for example, puts the total first. announce_separator is the punctuation between the parts of the screen-reader announcement (". " in English, "。" in Chinese and Japanese). If a language leaves a label out, that one label shows in English, so a missing translation never leaves a button unlabeled.
Right-to-left languages such as Arabic and Hebrew aren't supported yet. They need more than labels: the arrows, swipes, and slide directions would all have to be mirrored.
Why the lang attribute instead of a gallery option? Every page should set lang anyway: screen readers use it to choose the right voice and pronunciation, and browsers use it for spell checking and translation. Reading the same setting keeps the gallery's labels in step with the rest of the page, with nothing extra for you to configure.
10. Customizing the look
The gallery's look can be adjusted to suit your site with a few CSS variables. Set them on #tmtgContainer in your own stylesheet:
#tmtgContainer {
--tmtg-corner-radius: 6px;
--tmtg-font: Georgia, serif;
--tmtg-aspect: 16 / 9;
}
These are the variables meant for you to change:
| Variable | Default | What it changes |
|---|---|---|
--tmtg-background | #000 (black) | The gallery's backdrop: behind the image, the caption, and the thumbnail strip, and the bands beside images that don't fill the frame. |
--tmtg-foreground | #fff (white) | The caption text and the controls drawn on the backdrop: toolbar icons and border, slide number, strip arrows, thumbnail frame, and keyboard focus rings. The fainter parts are lighter mixes of this color, fitted to your pair (see "Colors" below). |
--tmtg-accent | rgb(100, 26, 0) (dark red) | The dark red behind the arrows and strip arrows on hover and after a tap. Its icons stay white, so it must be a dark color. |
--tmtg-corner-radius | 1.05em (17 pixels) | The rounded corners of the gallery, its toolbar, and the thumbnail frame, which all follow it. Match it to the corners of your site's cards or buttons, or set 0 for square corners. In fullscreen, the outer corners are always square, since the gallery fills the screen. |
--tmtg-aspect | 3 / 2 | The shape of the image area, as width / height, when the gallery has no set height (see "Sizing" in section 9). |
--tmtg-font | system-ui, sans-serif | The font of the captions and slide numbers (see "Font" in section 9). |
--tmtg-height | (none) | The gallery's height, like the "height" option (see "Sizing" in section 9). If both are set, the option wins. |
--tmtg-decorative-padding | .3em | The thickness of the frame around a decorative gallery's images (see "Decorative galleries" in section 9). |
Lengths can use any CSS unit. An "em" is the gallery's font size, normally 16 pixels, so 1.05em is about 17 pixels. Colors can be written any way CSS allows: a name, #hex, rgb(), hsl(), oklch(). A value the browser can't read as a color is ignored, and the default is used instead.
Why the #tmtgContainer ID? A rule written with the ID outranks the gallery's own rules, which use class names, so your values win whatever order the stylesheets load in. tmtg.css is added by the script after your page's own styles, so without the ID, the gallery's defaults would win and your settings would quietly do nothing.
Why variables, rather than overriding the gallery's rules directly? The rules inside tmtg.css change from version to version as the gallery improves, so a rule you copy and override may stop matching after an update. The variables above are kept on purpose: they're the settings that stay the same from one version to the next.
Colors
A dark backdrop with light controls suits most galleries, since photos stand out best on black, but a light one works too:
#tmtgContainer {
--tmtg-background: #fff;
--tmtg-foreground: #222;
}
Choose the pair with care: it needs a contrast of at least 7:1 (white on black is 21:1). In practice:
- with white controls, a backdrop no lighter than #595959;
- on a white backdrop, controls no lighter than #595959.
A free online contrast checker will tell you the ratio of any two colors.
Why 7:1, when WCAG asks only 4.5:1 of text? The gallery's fainter parts — the dimmed toolbar buttons, the strip arrows, the thumbnail frame, and the slide number on its pill — are lighter mixes of your foreground, and each must still reach what WCAG asks: 3:1 for icons and controls (1.4.11), 4.5:1 for text (1.4.3). A toggle button that's off is a dimmed version of one that's on, and that dimming is the only thing that tells the two apart on screen. Below about 7:1, once the dimmed button is strong enough to read, it looks almost the same as the one that's on.
The gallery fits the faint parts to your colors. When it starts, it reads the colors the browser is actually using and makes each faint part just strong enough to reach its WCAG target, never fainter than its usual strength. White on black, and most dark themes, need nothing changed, so they look exactly as before; a light theme gets slightly stronger dimmed buttons, strip arrows, or slide number. It fits them again every time the image changes, and whenever the device switches between light and dark mode, so a page whose colors follow that setting, or change some other way, stays readable.
Why fit them, rather than use fixed strengths? A faint part is your foreground mixed with transparency, and contrast falls off much faster on a light backdrop than on a dark one. A 50% white on black still has 5.3:1, but a 50% #444 on white has only 2.6:1, although #444 on white (9.7:1) is excellent for text. Earlier versions used fixed strengths chosen for white on black, so a light theme needed a pair of about 13:1, close to black on white, before every part passed.
If your pair is below 7:1, the gallery writes a warning in the browser's console (open the developer tools to see it), e.g.
TMTG: the contrast of --tmtg-foreground on --tmtg-background is
5.7:1; the gallery needs at least 7:1, so its dimmed buttons stay
readable and still look different from the ones that are on.
It still fits each part as well as it can. The check is there to catch a poor choice while you build the page, because a low-contrast gallery looks fine to the person who chose the colors and can be unreadable to visitors with low vision, or anyone in bright sunlight.
Things to know:
- Fullscreen always uses a black backdrop with white controls, whatever colors you set. In fullscreen the image is all there is to see, and the page your colors were chosen to match is out of sight. The accent is kept.
- The prev/next arrows and the numbers on the thumbnails don't change. They sit over the images, which can be any color, so they carry their own dark backing that works over any picture.
- A see-through backdrop (e.g. rgba(0, 0, 0, .5)) shows your page behind it. The gallery can't know that color, so it fits the faint parts for the worst case, over black and over white, and may warn even when your page is fine.
- The accent sits behind white icons, over images as well as the backdrop, so it's checked against the hardest case, a white image. Dark, rich colors work; pale ones don't.
Other variables
tmtg.css uses other variables too, such as --tmtg-toolbar-clearance, --tmtg-edge-gap, --tmtg-arrow-clearance, and --tmtg-thumb-height. They aren't meant to be changed. Each is worked out from the others, or from the gallery's width, so the toolbar, image, caption, and thumbnail strip line up; changing one throws the spacing off, and a narrow gallery replaces several of them with its own values anyway. They may also be renamed or removed in a later version. The same goes for --tmtg-color-bg, --tmtg-color-fg, and --tmtg-color-accent: they're the gallery's own copies of your colors, which fullscreen replaces with black and white, so set the three color variables above instead.
--tmtg-object-fit is set by the script from the "objectFit" option, which replaces any value from your stylesheet. Use the option instead (section 3).
11. License
TMTG is free software under the MIT License. The full text is in the LICENSE.txt file that comes with the gallery, and the header of each file names it.
What it allows: anyone may use the gallery on any site, personal, commercial, or government, and copy, change, or share it, including as part of something they sell, without asking or paying.
What it asks: keep the copyright line and the license with every copy. tmtg.js and tmtg.css carry them in their header comments, and the minified files carry a one-line version at the top, which the build keeps, so copying the files as they are is enough. If you change the files, leave those lines in.
What it doesn't give: any warranty. The gallery is provided as it is, and the author isn't liable for problems it causes.
Why MIT? It's the most widely used license for small JavaScript libraries, so the people who look after websites, and their lawyers, usually know it already. It asks almost nothing of the sites that use the gallery, which suits a script made to be dropped into other people's pages, while keeping the author's name on the code. Licenses that require changes to be shared back (such as the GPL) would put many sites off using it at all.
12. Feedback and bug reports
TMTG is written and looked after by Timothy Beto. Bug reports, feature requests, and questions are welcome by email: tim@timcode.dev.
The latest version, with the demo and these documents, is at https://timcode.dev/tmtg. Why say so here, and in the headers of the script and stylesheet? Files get copied from site to site and lose track of where they came from, and this address leads back to newer versions and fixes.
A bug report is easiest to act on when it says:
- what you did, what you expected, and what happened instead;
- the browser and its version, and the device (e.g. Safari 18 on an iPhone 15, or Firefox 140 on Windows 11);
- the TMTG version, from the header of
tmtg.jsor the minified file's name; - any warnings starting with "TMTG:" in the browser's developer console (F12);
- if you can, the address of a page that shows the problem, or a small page that does.
Why so much detail? Most gallery problems depend on the browser, the device, or the page around the gallery: a layout that's fine in Chrome on a laptop can come apart in Safari on a phone, or inside a page's own grid. Knowing exactly where it happened is usually most of the work of finding it.
Native speakers are especially welcome to check the control labels in their language (see "Language" in section 9). The Chinese, Japanese, Korean, and Hindi labels haven't been reviewed by one yet.
You're also welcome to send the labels in your own language, if TMTG doesn't have it yet, to be added to the gallery with credit to you. There are eleven of them (Gallery controls, Auto-advance, Captions, Thumbnails, Full screen, Previous image, Next image, Show image {n} of {total}, Back five images, Forward five images, Image {n} of {total}); the gallery fills in {n} and {total} with numbers, so put them wherever your language's grammar wants them.
Code contributions (pull requests) aren't accepted. Feature requests that fit the gallery may go into a future release.