Design & Media

Contextual & adaptive UI

Smart interfaces tailor their content and calls-to-action to the user's situation — the time of day, where they are, their device and environment, and whether they're new or returning. Below, each signal is detected live, with the JavaScript to do it yourself.

Start here

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".

Live · signal 1

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…

HTML
<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>
CSS
.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; }
JavaScript
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
Live · signal 2

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.

HTML
<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>
CSS
.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; }
JavaScript
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();
};
Live · signal 3

Environment & device

CSS media queries and a few browser APIs read the user's environment — here's yours, detected live:

Prefers dark mode
Prefers reduced motion
Input type
Connection
Viewport
HTML
<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>
CSS
.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; }
JavaScript
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
CSS (the no-JS version)
@media (prefers-color-scheme: dark) { :root { --bg:#111; --fg:#eee; } }
@media (pointer: coarse) { .btn { min-height: 48px; } }   /* bigger touch targets */
Live · signal 4

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:

HTML
<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>
CSS
.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; }
JavaScript
// 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 */ });
Always ask, always fall back: never request location without a clear reason, and design a good default for when it's declined or unavailable.
Live · putting it together

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:

Tailored for you

HTML
<div class="acta">
  <div class="label">Tailored for you</div>
  <h3 id="actaTitle">…</h3>
  <a href="#" id="actaBtn">…</a>
</div>
CSS
.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; }
JavaScript
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;
In practice

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
Related: for the layout side of adapting (responsive grids, container queries), see Modular & Adaptive UI; for the toggle pattern, Dark / Light Mode; and UX Best Practices.
Recap

The takeaway

Contextual UI reads the situationnew 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.