What is contextual / adaptive UI?
It's an interface that changes based on who's looking and when — not just screen size. The same page can greet a morning visitor differently than a night owl, welcome a returning user, and adjust to a phone in bright sunlight. Used well, it feels personal; used carelessly, it feels creepy — so always be transparent and provide a sensible default.
Time of day
Morning, afternoon, or night — change the greeting, theme, and featured content.
Location
Timezone, region, language, or currency — show what's relevant where they are.
Environment
Dark mode, reduced motion, touch vs mouse, online/offline, screen size.
New vs returning
First-timers see "Get started"; returning users see "Welcome back".
Time of day
This greeting changes right now based on your device's clock — morning, afternoon, or evening, each with its own icon and color.
Hello!
Reading the clock…
<div class="greet" id="greet">
<i class="ph-duotone ph-sun" id="greetIcon"></i>
<div>
<h3 id="greetTitle">Hello!</h3>
<p id="greetSub">Reading the clock…</p>
</div>
</div>
.greet { border-radius: 14px; padding: 26px; display: flex; align-items: center; gap: 18px; transition: background .4s; }
.greet i { font-size: 2.6rem; }
.greet h3 { font-size: 1.4rem; }
.greet p { font-size: .88rem; opacity: .85; }
.greet.morning { background: linear-gradient(135deg,#fef3c7,#fde68a); color: #92400e; }
.greet.afternoon { background: linear-gradient(135deg,#dbeafe,#bfdbfe); color: #1e40af; }
.greet.evening { background: linear-gradient(135deg,#312e81,#1e1b4b); color: #e0e7ff; }
const h = new Date().getHours();
let part, icon;
if (h < 12) { part = 'morning'; icon = 'ph-sun-horizon'; }
else if (h < 18) { part = 'afternoon'; icon = 'ph-sun'; }
else { part = 'evening'; icon = 'ph-moon-stars'; }
const greet = document.getElementById('greet');
greet.className = 'greet ' + part; // swap the theme
document.getElementById('greetTitle').textContent = 'Good ' + part + '!';
document.getElementById('greetIcon').className = 'ph-duotone ' + icon; // swap the Phosphor icon
New vs returning visitor
We remember you with localStorage. First visit shows a welcome + "Get started"; come back and it greets you differently with a "Continue" CTA. Use the button to reset and see both.
<div class="visitor-box" id="visitorBox"> <div class="msg" id="visitorMsg">…</div> <div id="visitorCtaWrap"></div> <button class="btn ghost" id="visitorReset">↺ Reset (forget me)</button> </div>
.visitor-box { text-align: center; padding: 8px; }
.visitor-box .msg { font-size: 1.2rem; font-weight: 700; margin-bottom: 6px; }
.visitor-box .cta { display: inline-block; background: #8b5cf6; color: #fff; border-radius: 999px; padding: 10px 22px; font-weight: 700; margin-top: 8px; }
.btn { border: none; background: #8b5cf6; color: #fff; font-weight: 700; padding: 10px 18px; border-radius: 9px; cursor: pointer; margin-top: 14px; }
.btn.ghost { background: #f5f3ff; color: #6d28d9; }
const msg = document.getElementById('visitorMsg');
const wrap = document.getElementById('visitorCtaWrap');
if (localStorage.getItem('cx_visited')) {
msg.textContent = 'Welcome back! 👋';
wrap.innerHTML = '<span class="cta">Continue where you left off</span>';
} else {
msg.textContent = 'Welcome — first time here?';
wrap.innerHTML = '<span class="cta">Get started free</span>';
localStorage.setItem('cx_visited', '1'); // remember for next time
}
document.getElementById('visitorReset').onclick = () => {
localStorage.removeItem('cx_visited');
location.reload();
};
Environment & device
CSS media queries and a few browser APIs read the user's environment — here's yours, detected live:
<div class="env" id="env"> <div class="env-row"><i class="ph-duotone ph-moon-stars"></i> Prefers dark mode <b data-scheme>…</b></div> <div class="env-row"><i class="ph-duotone ph-person-simple-walk"></i> Prefers reduced motion <b data-motion>…</b></div> <div class="env-row"><i class="ph-duotone ph-cursor"></i> Input type <b data-pointer>…</b></div> <div class="env-row"><i class="ph-duotone ph-wifi-high"></i> Connection <b data-online>…</b></div> <div class="env-row"><i class="ph-duotone ph-arrows-out"></i> Viewport <b data-size>…</b></div> </div>
.env { display: grid; gap: 8px; }
.env-row { display: flex; align-items: center; gap: 10px; background: #f6f7f9; border-radius: 9px; padding: 10px 14px; font-size: .9rem; }
.env-row i { color: #8b5cf6; font-size: 1.2rem; }
.env-row b { margin-left: auto; color: #111; font-family: 'SF Mono', Consolas, monospace; font-size: .82rem; }
const env = document.getElementById('env');
const ask = q => window.matchMedia(q).matches;
function update() {
env.querySelector('[data-scheme]').textContent = ask('(prefers-color-scheme: dark)') ? 'Yes' : 'No';
env.querySelector('[data-motion]').textContent = ask('(prefers-reduced-motion: reduce)') ? 'Reduced' : 'Full';
env.querySelector('[data-pointer]').textContent = ask('(pointer: coarse)') ? 'Touch' : 'Mouse';
env.querySelector('[data-online]').textContent = navigator.onLine ? 'Online' : 'Offline';
env.querySelector('[data-size]').textContent = innerWidth + ' × ' + innerHeight;
}
update();
window.addEventListener('resize', update); // react live to changes
@media (prefers-color-scheme: dark) { :root { --bg:#111; --fg:#eee; } }
@media (pointer: coarse) { .btn { min-height: 48px; } } /* bigger touch targets */
Location
Your timezone is detected automatically (no permission needed) — a quick clue to region. For precise location you ask permission with the Geolocation API. Below, pick a region to see content adapt (currency & greeting):
Detected timezone: …
<p><i class="ph-duotone ph-globe-hemisphere-west"></i> Detected timezone: <b id="tz">…</b></p>
<div class="loc-row">
<label>Simulate region:</label>
<select id="regionSel">
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="jp">Japan</option>
<option value="de">Germany</option>
</select>
</div>
<div class="loc-out" id="locOut">…</div>
.loc-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 12px; }
.loc-row select { padding: 9px 12px; border: 1px solid #d1d5db; border-radius: 8px; font: inherit; }
.loc-out { background: #f5f3ff; border-radius: 10px; padding: 14px 16px; color: #5b21b6; font-size: .9rem; }
// timezone — instant, no permission
document.getElementById('tz').textContent =
Intl.DateTimeFormat().resolvedOptions().timeZone; // e.g. "America/New_York"
// simulate a region → adapt currency & greeting
const data = {
us: { cur: '$ USD', hi: 'Hello! Free shipping over $50.' },
uk: { cur: '£ GBP', hi: 'Hiya! Free delivery over £40.' },
jp: { cur: '¥ JPY', hi: 'こんにちは!¥5,000以上で送料無料。' },
de: { cur: '€ EUR', hi: 'Hallo! Kostenloser Versand ab 45 €.' }
};
const sel = document.getElementById('regionSel');
const out = document.getElementById('locOut');
function show() { const d = data[sel.value]; out.innerHTML = '<b>' + d.cur + '</b> — ' + d.hi; }
sel.addEventListener('change', show);
show();
// precise location — asks the user's permission (optional)
// navigator.geolocation.getCurrentPosition(
// pos => { const { latitude, longitude } = pos.coords; /* look up region */ },
// err => { /* user declined — fall back to a sensible default */ });
An adaptive call-to-action
Combine the signals and the CTA writes itself — this one blends your time of day and whether you're new or returning, live:
<div class="acta"> <div class="label">Tailored for you</div> <h3 id="actaTitle">…</h3> <a href="#" id="actaBtn">…</a> </div>
.acta { text-align: center; border-radius: 14px; padding: 26px; background: linear-gradient(135deg,#0f172a,#3b0764); color: #fff; }
.acta .label { font-size: .7rem; text-transform: uppercase; letter-spacing: .14em; color: #c4b5fd; }
.acta h3 { font-size: 1.3rem; margin: 6px 0 12px; }
.acta a { display: inline-block; background: linear-gradient(135deg,#8b5cf6,#22d3ee); color: #fff; border-radius: 999px; padding: 12px 26px; font-weight: 800; text-decoration: none; }
const hour = new Date().getHours();
const returning = !!localStorage.getItem('cx_visited');
let title, cta;
if (returning) { title = 'Welcome back!'; cta = 'Pick up where you left off'; }
else if (hour < 12) { title = 'Good morning ☀️'; cta = 'Start your day — try it free'; }
else if (hour >= 18) { title = 'Winding down?'; cta = 'Save something for later'; }
else { title = 'Got a few minutes?'; cta = 'Get started free'; }
document.getElementById('actaTitle').textContent = title; // one CTA, tailored to the moment
document.getElementById('actaBtn').textContent = cta;
Do & don't
Do
- Always design a sensible default first
- Ask permission for location, with a reason
- Use easy signals (time, timezone, returning) before invasive ones
- Honor dark-mode & reduced-motion preferences
- Let users override what you guessed
Don't
- Demand location/permissions on arrival
- Break the page if a signal is missing
- Make adaptation feel creepy or hidden
- Assume a guess is correct — let users change it
- Store personal data without consent
The takeaway
Contextual UI reads the situation — new Date() for time of day,
localStorage for new vs returning, matchMedia() for environment, and
Intl/Geolocation for place — then tailors the content and CTA. Always start from a solid
default, ask before you track, and let people override your guess.