09Team footer — a row computing each member's local time from their time zone
Feeding each member's stored IANA time zone string into Intl.DateTimeFormat's timeZone option computes that same instant as their own local time, and setInterval advances a simulated clock by 95 minutes per tick and recomputes; a quiet marker gets layered on for any member whose computed time falls inside late-night hours.
Best forRemote-team intro footers, global team contact info
Intl.DateTimeFormatsetIntervaltimeZone

<main class="stage">
<section class="foot" id="foot">
<p class="foot__hint" id="footHint">It adds a simulated elapsed time to a fixed reference moment to calculate each member's local time across different time zones.</p>
<div class="foot__row" id="footRow" role="list" aria-label="Team member local time"></div>
<p class="foot__meta"><span class="foot__chip" id="footChip">Reference 2026-01-01 09:00 UTC</span></p>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-base; // 300ms — 조용 상태 전환
$easing: $ease-out;
$color: $subject-blue-deep;
@include stage($stage-ink);
.foot {
width: 100%;
max-width: 420px;
display: flex;
flex-direction: column;
gap: $sp-3;
align-items: center;
text-align: center;
}
.foot__hint {
margin: 0;
padding: 0 $sp-2;
font-weight: 400;
font-size: 11px;
line-height: 1.4;
color: rgba($subject-cream, .72);
}
.foot__row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: $sp-2;
width: 100%;
}
.foot__cell {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-1;
padding: $sp-2;
border-radius: $r-control;
background: rgba($subject-cream, .08);
transition: background $duration $easing;
}
.foot__cell.is-quiet {
background: rgba($subject-cream, .03);
}
.foot__tag {
font-weight: 500;
font-size: 9px;
color: rgba($subject-cream, .72);
}
.foot__time {
font-weight: 700;
font-size: 14px;
color: $subject-cream;
font-variant-numeric: tabular-nums;
}
.foot__dot {
display: block;
width: 6px;
height: 6px;
border-radius: $r-pill;
background: $color;
opacity: 0;
transition: opacity $duration $easing;
}
.foot__cell.is-quiet .foot__dot {
opacity: 1;
background: rgba($subject-cream, .45);
}
.foot__meta { margin: 0; }
.foot__chip {
display: inline-flex;
padding: $sp-1 $sp-3;
border-radius: $r-pill;
background: $color;
color: $subject-cream;
font-weight: 700;
font-size: 11px;
}
@media (max-width: 340px) {
.foot { max-width: 300px; gap: $sp-2; }
.foot__hint { font-size: 9px; }
.foot__row { gap: $sp-1; }
.foot__time { font-size: 12px; }
.foot__chip { font-size: 10px; padding: $sp-1 $sp-2; }
}
@media (prefers-reduced-motion: reduce) {
.foot__cell,
.foot__dot {
transition: none;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.foot {
width: 100%;
max-width: 420px;
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
text-align: center;
}
.foot__hint {
margin: 0;
padding: 0 8px;
font-weight: 400;
font-size: 11px;
line-height: 1.4;
color: rgba(255, 247, 230, 0.72);
}
.foot__row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
width: 100%;
}
.foot__cell {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 8px;
border-radius: 12px;
background: rgba(255, 247, 230, 0.08);
transition: background 300ms ease-out;
}
.foot__cell.is-quiet {
background: rgba(255, 247, 230, 0.03);
}
.foot__tag {
font-weight: 500;
font-size: 9px;
color: rgba(255, 247, 230, 0.72);
}
.foot__time {
font-weight: 700;
font-size: 14px;
color: #fff7e6;
font-variant-numeric: tabular-nums;
}
.foot__dot {
display: block;
width: 6px;
height: 6px;
border-radius: 999px;
background: #0a4fe8;
opacity: 0;
transition: opacity 300ms ease-out;
}
.foot__cell.is-quiet .foot__dot {
opacity: 1;
background: rgba(255, 247, 230, 0.45);
}
.foot__meta {
margin: 0;
}
.foot__chip {
display: inline-flex;
padding: 4px 12px;
border-radius: 999px;
background: #0a4fe8;
color: #fff7e6;
font-weight: 700;
font-size: 11px;
}
@media (max-width: 340px) {
.foot {
max-width: 300px;
gap: 8px;
}
.foot__hint {
font-size: 9px;
}
.foot__row {
gap: 4px;
}
.foot__time {
font-size: 12px;
}
.foot__chip {
font-size: 10px;
padding: 4px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.foot__cell,
.foot__dot {
transition: none;
}
}// Team member local time 푸터(09) — 고정 기준시각(EPOCH)에 가상의 경과시간을 더한 시각 하나를
// Intl.DateTimeFormat 으로 팀원마다 다른 timeZone 에 맞춰 다시 읽는다(계산 한 겹).
// 그 시각의 시(hour) 가 야간대(22~6시)에 들면 조용 표시를 켠다. 실제 Date.now() 는
// 캡처 재현성을 위해 쓰지 않는다.
(function () {
var row = document.getElementById('footRow');
var chip = document.getElementById('footChip');
if (!row || !chip) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var EPOCH = new Date('2026-01-01T09:00:00Z').getTime(); // 고정 기준시각
var STEP_MIN = 95; // 가상 elapsed — 매 tick 마다 이만큼 시계를 돌린다
var MEMBERS = [
{ tag: 'M1', zone: 'Asia/Seoul' },
{ tag: 'M2', zone: 'Europe/London' },
{ tag: 'M3', zone: 'America/New_York' },
{ tag: 'M4', zone: 'Australia/Sydney' }
];
MEMBERS.forEach(function (m) {
var cell = document.createElement('div');
cell.className = 'foot__cell';
cell.setAttribute('role', 'listitem');
var tag = document.createElement('span');
tag.className = 'foot__tag';
tag.textContent = m.tag;
var time = document.createElement('time');
time.className = 'foot__time';
var dot = document.createElement('span');
dot.className = 'foot__dot';
dot.setAttribute('aria-hidden', 'true');
cell.appendChild(tag);
cell.appendChild(time);
cell.appendChild(dot);
row.appendChild(cell);
m.cell = cell;
m.timeEl = time;
});
function hourIn(zone, date) {
var s = new Intl.DateTimeFormat('en-GB', { timeZone: zone, hour: 'numeric', hourCycle: 'h23' }).format(date);
return parseInt(s, 10);
}
var elapsedMin = 0;
function render() {
var now = new Date(EPOCH + elapsedMin * 60000);
MEMBERS.forEach(function (m) {
var label = new Intl.DateTimeFormat('en-GB', { timeZone: m.zone, hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).format(now);
m.timeEl.textContent = label;
m.timeEl.setAttribute('datetime', now.toISOString());
var hour = hourIn(m.zone, now);
var quiet = hour < 7 || hour >= 22;
m.cell.classList.toggle('is-quiet', quiet);
});
chip.textContent = 'elapsed ' + elapsedMin + ' min';
}
render();
if (!reduce) {
setInterval(function () {
elapsedMin += STEP_MIN;
render();
}, 550);
}
})();// TeamFooter — vanilla 판(run/338/demo/09-team-footer/index.html) 의 React 포트.
// 고정 기준시각에 가상의 경과 시간을 더해 팀원마다 다른 시간대의 현지시각을
// 계산한다(계산 한 겹).
import { useEffect, useRef } from "react";
import styles from "./TeamFooter.module.scss";
export type TeamFooterProps = {
duration?: string;
easing?: string;
color?: string;
};
const HINT = "고정 기준시각에 가상의 경과 시간을 더해 팀원마다 다른 시간대의 현지시각을 계산합니다";
const ROW_LABEL = "팀원 현지시각";
const EPOCH = new Date("2026-01-01T09:00:00Z").getTime();
const STEP_MIN = 95;
type Member = { tag: string; zone: string };
const MEMBERS: Member[] = [
{ tag: "M1", zone: "Asia/Seoul" },
{ tag: "M2", zone: "Europe/London" },
{ tag: "M3", zone: "America/New_York" },
{ tag: "M4", zone: "Australia/Sydney" }
];
function hourIn(zone: string, date: Date): number {
const fmt = new Intl.DateTimeFormat("en-GB", { timeZone: zone, hour: "numeric", hourCycle: "h23" });
return parseInt(fmt.format(date), 10) || 0;
}
export default function TeamFooter({
duration = "300ms",
easing = "ease-out",
color = "#0a4fe8"
}: TeamFooterProps) {
const rowRef = useRef<HTMLDivElement | null>(null);
const chipRef = useRef<HTMLSpanElement | null>(null);
useEffect(() => {
const row = rowRef.current;
const chip = chipRef.current;
if (!row || !chip) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const cells = Array.prototype.slice.call(row.querySelectorAll("." + styles.cell)) as HTMLElement[];
const timeEls = Array.prototype.slice.call(row.querySelectorAll("." + styles.time)) as HTMLElement[];
let elapsedMin = 0;
function render(): void {
const now = new Date(EPOCH + elapsedMin * 60000);
MEMBERS.forEach((m, i) => {
const fmt = new Intl.DateTimeFormat("en-GB", {
timeZone: m.zone,
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23"
});
const label = fmt.format(now);
const timeEl = timeEls[i];
const cell = cells[i];
if (!timeEl || !cell) return;
timeEl.textContent = label;
timeEl.setAttribute("datetime", now.toISOString());
const hour = hourIn(m.zone, now);
const quiet = hour < 7 || hour >= 22;
cell.classList.toggle(styles.isQuiet, quiet);
});
chip!.textContent = "경과 " + elapsedMin + "분";
}
render();
let timer = 0;
if (!reduce) {
timer = window.setInterval(() => {
elapsedMin += STEP_MIN;
render();
}, 550);
}
return () => {
if (timer) window.clearInterval(timer);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<main className={styles.stage}>
<section className={styles.foot}>
<p className={styles.hint}>{HINT}</p>
<div className={styles.row} ref={rowRef} role="list" aria-label={ROW_LABEL}>
{MEMBERS.map((m) => (
<div className={styles.cell} role="listitem" key={m.tag}>
<span className={styles.tag}>{m.tag}</span>
<time className={styles.time}>09:00</time>
<span className={styles.dot} aria-hidden="true" />
</div>
))}
</div>
<p className={styles.meta}>
<span className={styles.chip} ref={chipRef} style={{ color }}>경과 0분</span>
</p>
</section>
</main>
);
}09 Team footer — a row computing each member's local time from their time zone — Measured area 1.403% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · 9 Team Page Design Patterns (Copy-Paste)
The post this demo lives in
9 Team Page Design Patterns (Copy-Paste)Nine team-page sections that compute their layout: columns balance by headcount, bios get read-more only past overflow, See all demos in this post
If you like this effect - similar demos
Landing sections09 Case footer — a summary badge that works out the average across every story seen so far9 Case Study Page Design Sections (Copy-Paste)
Landing sections09 Countdown band9 Event Landing Page Templates — Copy-Paste
Landing sections03 Feature highlight9 Promotion Landing Page Templates — Copy-Paste