
Outdo Airports
The UK leader in regional airport advertising, offering unrivalled scale, visibility, and value. We connect brands with high-value travellers at moments when attention is at its peak.

Outdo Roadside
From roundabouts and bridges to lamppost banners and welcome signs, our nationwide network delivers hyper-local visibility with national impact, and puts your brand in front of people.

Outdo Transport
Bus and tram advertising brings your message to the heart of communities across the UK. With unmatched reach and local relevance, our formats are always on the move, because so are your customers.
(function () {
const AUTOPLAY_MS = 4000;
const DRAG_THRESHOLD = 12; // px before we treat it as a drag
const TAP_MAX_TIME = 350; // ms. if it ends quickly and under threshold, treat as tap
const S = {
component: ".timed-slider_component",
navButton: ".timed-slider_nav-button",
slidesWrap: ".timed-slider_slides",
slide: ".timed-slider_slide",
bar: ".timed-slider_progress-bar",
activeClass: "is--active",
};
document.querySelectorAll(S.component).forEach(initSlider);
function initSlider(root) {
const wrap = root.querySelector(S.slidesWrap);
const slides = Array.from(root.querySelectorAll(S.slide));
const buttons = Array.from(root.querySelectorAll(S.navButton));
const bars = buttons.map(b => b.querySelector(S.bar));
if (!wrap || slides.length === 0 || buttons.length !== slides.length) return;
const DEFAULT_TRANSITION = "transform .6s cubic-bezier(.22,.7,.26,1)";
if (!wrap.style.transition) {
wrap.style.display = "grid";
wrap.style.gridAutoFlow = "column";
wrap.style.gridAutoColumns = "100%";
wrap.style.willChange = "transform";
wrap.style.transition = DEFAULT_TRANSITION;
}
wrap.style.touchAction = "pan-y";
let index = Math.max(0, buttons.findIndex(b => b.classList.contains(S.activeClass)));
if (index === -1) index = 0;
let rafId = null;
let startTime = 0;
function go(i, resetProgress = true) {
index = (i + slides.length) % slides.length;
setActive(index);
wrap.style.transition = DEFAULT_TRANSITION;
wrap.style.transform = `translateX(${(-100 * index)}%)`;
if (resetProgress) startProgress();
}
function setActive(i) {
buttons.forEach((btn, bi) => {
btn.classList.toggle(S.activeClass, bi === i);
btn.setAttribute("aria-current", bi === i ? "true" : "false");
});
bars.forEach((bar) => {
if (!bar) return;
bar.style.width = "0%";
});
}
function startProgress() {
cancelAnimationFrame(rafId);
const bar = bars[index];
if (!bar) return;
startTime = performance.now();
function step(now) {
const t = Math.min(1, (now - startTime) / AUTOPLAY_MS);
bar.style.width = (t * 100).toFixed(3) + "%";
if (t >= 1) {
go(index + 1);
return;
}
rafId = requestAnimationFrame(step);
}
rafId = requestAnimationFrame(step);
}
buttons.forEach((btn, i) => {
btn.addEventListener("click", () => go(i));
});
// Drag and swipe with proper tap detection
let dragging = false;
let suppressNextClick = false;
let startX = 0;
let deltaX = 0;
let baseX = 0;
let pointerId = null;
let downAt = 0;
function slideWidth() {
return slides[0].getBoundingClientRect().width;
}
function isInteractive(el) {
if (!el) return false;
const tag = el.tagName;
if (["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA", "LABEL"].includes(tag)) return true;
return el.closest("a,button,[role='button'],input,select,textarea,label") != null;
}
function onPointerDown(e) {
// Allow vertical scroll to start from slider, and allow taps on interactive items
pointerId = e.pointerId ?? null;
downAt = performance.now();
startX = e.clientX ?? (e.touches && e.touches[0].clientX) ?? 0;
baseX = -index * slideWidth();
deltaX = 0;
dragging = false; // we only flip to true after threshold
cancelAnimationFrame(rafId);
// We will capture only once drag starts, not immediately
window.addEventListener("pointermove", onPointerMove, { passive: true });
window.addEventListener("pointerup", onPointerUp, { once: true });
window.addEventListener("pointercancel", onPointerCancel, { once: true });
}
function onPointerMove(e) {
const x = e.clientX ?? (e.touches && e.touches[0].clientX) ?? startX;
deltaX = x - startX;
// Only start dragging after threshold and when the gesture is primarily horizontal
if (!dragging && Math.abs(deltaX) > DRAG_THRESHOLD) {
// If the press started on an interactive element, still allow drag once the user clearly drags
dragging = true;
suppressNextClick = true; // we will suppress the immediate click after a real drag
wrap.style.transition = "none";
try {
if (wrap.setPointerCapture && pointerId != null) wrap.setPointerCapture(pointerId);
} catch (_) {}
}
if (dragging) {
wrap.style.transform = `translateX(${baseX + deltaX}px)`;
}
}
function onPointerUp() {
window.removeEventListener("pointermove", onPointerMove);
const elapsed = performance.now() - downAt;
if (!dragging) {
// It was a tap. No suppression unless we barely crossed threshold extremely quickly.
const wasTinyMove = Math.abs(deltaX) <= DRAG_THRESHOLD;
const wasQuick = elapsed <= TAP_MAX_TIME;
suppressNextClick = !(wasTinyMove && wasQuick) ? false : false;
startProgress();
return;
}
// Snap if we were dragging
const width = slideWidth();
const threshold = width * 0.2;
let target = index;
if (deltaX <= -threshold) target = index + 1;
else if (deltaX >= threshold) target = index - 1;
go(target);
}
function onPointerCancel() {
window.removeEventListener("pointermove", onPointerMove);
dragging = false;
suppressNextClick = false;
startProgress();
}
// Only suppress clicks if a real drag happened
wrap.addEventListener(
"click",
function (e) {
if (!suppressNextClick) return;
// If user meant to click an interactive element but just dragged, block this one click
e.preventDefault();
e.stopPropagation();
suppressNextClick = false; // only consume once
},
true // capture to intercept before links fire
);
wrap.addEventListener("pointerdown", onPointerDown);
window.addEventListener("resize", () => {
wrap.style.transition = "none";
wrap.style.transform = `translateX(${(-100 * index)}%)`;
startProgress();
});
// Init
go(index);
}
})();