CSSとJSで作る、世界の時間を円で見る回転時計

アニメーション

CSSとJSで作る、世界の時間を円で見る回転時計

投稿日2026/02/11

更新日2026/2/11

デジタル時計は便利だけど、時間の流れそのものはあまり感じられません。
そこで今回は、時間を「数字」ではなく「円の回転」で見せる時計を作ります。

秒・分・時刻だけでなく、日付・月・年・曜日までもリングとして重ね、現在の値に合わせてゆっくり回転。
さらに中央の国旗ボタンから言語とタイムゾーンを切り替えることで、世界の“いま”をワンタップで覗けるインタラクティブな時計になっています。

Preview プレビュー

Code コード

<div class="kumonosu-clock">
	<div>
		<div data-clock="years" data-numbers="101" class="kumonosu-clock-face"></div>
	</div>
	<div>
		<div data-clock="seconds" data-numbers="60" class="kumonosu-clock-face"></div>
	</div>
	<div>
		<div data-clock="minutes" data-numbers="60" class="kumonosu-clock-face"></div>
	</div>
	<div>
		<div data-clock="hours" data-numbers="24" class="kumonosu-clock-face"></div>
	</div>
	<div>
		<div data-clock="days" data-numbers="31" class="kumonosu-clock-face"></div>
	</div>
	<div>
		<div data-clock="months" data-numbers="12" class="kumonosu-clock-face"></div>
	</div>
	<div>
		<div data-clock="day-names" data-numbers="7" class="kumonosu-clock-face"></div>
	</div>
	<button type="button" id="kumonosu-current-lang" class="kumonosu-current-lang-display">ja</button>
</div>
<dialog id="kumonosu-language-dialog">
	<button type="button" id="kumonosu-btn-dialog-close" class="kumonosu-btn-dialog-close" autofocus>✕</button>
	<div id="kumonosu-language-options" class="kumonosu-language-options"></div>
</dialog>
*,
::before,
::after {
	box-sizing: border-box;
}
:root {
	--bg-clr: rgb(255 255 255);
	--clock-size: 800px;
	--theme-color: #b30000;
	--clock-numbers-clr: #000000;
	--active-numbers-clr: #000000;
	--clock-mask-opacity: .85;
	--clock-mask-inset: 2px;
	--clock-dialog-bg-clr: rgba(255, 255, 255, 0.9);
}
body {
	margin: 0;
	min-height: 100svh;
	display: grid;
	place-content: center;
	font-family: system-ui, -apple-system, sans-serif;
	background-color: var(--bg-clr);
	transition: background-color 500ms ease;
	overflow: hidden;
}
.kumonosu-clock {
	position: fixed;
	place-content: center;
	inset: 0;
	margin: auto;
	width: var(--clock-size);
	height: var(--clock-size);
	aspect-ratio: 1;
	border-radius: 50%;
}
@media (width < 800px) {
	.kumonosu-clock {
		left: 0;
		right: auto;
		translate: calc((50% - 2rem) * -1) 0;
	}
}
.kumonosu-clock::before {
	content: "";
	position: absolute;
	inset: var(--clock-mask-inset);
	margin: auto;
	border-radius: 50%;
	z-index: 20;
	clip-path: polygon(0 0,
			100% 0,
			100% 48%,
			50% 48%,
			50% 52%,
			100% 52%,
			100% 100%,
			0 100%);
	background-color: var(--theme-color);
	opacity: var(--clock-mask-opacity, .75);
	transition: background-color 500ms ease;
}
.kumonosu-clock>div {
	position: absolute;
	inset: 0;
	margin: auto;
	width: var(--kumonosu-clock-d);
	height: var(--kumonosu-clock-d);
	font-size: var(--f-size, 0.9rem);
	aspect-ratio: 1;
	isolation: isolate;
	border-radius: 50%;
}
.kumonosu-clock>div:nth-of-type(1) {
	--kumonosu-clock-d: calc(var(--clock-size) - 20px);
}
.kumonosu-clock>div:nth-of-type(2) {
	--kumonosu-clock-d: calc(var(--clock-size) - 130px);
}
.kumonosu-clock>div:nth-child(3) {
	--kumonosu-clock-d: calc(var(--clock-size) - 195px);
}
.kumonosu-clock>div:nth-child(4) {
	--kumonosu-clock-d: calc(var(--clock-size) - 260px);
}
.kumonosu-clock>div:nth-child(5) {
	--kumonosu-clock-d: calc(var(--clock-size) - 350px);
}
.kumonosu-clock>div:nth-child(6) {
	--kumonosu-clock-d: calc(var(--clock-size) - 470px);
}
.kumonosu-clock>div:nth-child(7) {
	--kumonosu-clock-d: calc(var(--clock-size) - 600px);
}
.kumonosu-clock-face {
	position: relative;
	width: 100%;
	height: 100%;
	aspect-ratio: 1;
	border-radius: 50%;
	transition: 300ms linear;
}
.kumonosu-clock-face>* {
	position: absolute;
	transform-origin: center;
	white-space: nowrap;
	color: var(--clock-numbers-clr);
	opacity: 0.75;
	transition: color 500ms ease;
}
.kumonosu-clock-face>.kumonosu-active {
	opacity: 1;
	font-weight: bold;
	color: var(--active-numbers-clr);
}
.kumonosu-clock>.kumonosu-current-lang-display {
	position: absolute;
	inset: 0;
	margin: auto;
	z-index: 100;
	display: grid;
	place-content: center;
	background-color: white;
	border: 2px solid var(--theme-color);
	border-radius: 50%;
	width: 44px;
	height: 44px;
	cursor: pointer;
	transition: 300ms ease-in-out;
	font-size: 1.5rem;
	outline: none;
}
.kumonosu-current-lang-display::before,
.kumonosu-current-lang-display::after {
	content: ":";
	color: var(--clock-numbers-clr);
	position: absolute;
	z-index: 199;
	top: 50%;
	right: 0;
	font-size: 0.9rem;
	translate: 283px -10px;
}
.kumonosu-current-lang-display::after {
	translate: 250px -10px;
}
#kumonosu-language-dialog {
	width: min(calc(100% - 2rem), 380px);
	padding: 1rem;
	border: none;
	border-radius: 50%;
	background: var(--clock-dialog-bg-clr);
	text-align: center;
	aspect-ratio: 1;
	overflow: visible;
	transition: opacity 500ms ease-in, scale 500ms cubic-bezier(0.28, -0.55, 0.27, 1.55);
}
#kumonosu-language-dialog:not([open]) {
	opacity: 0;
	scale: 0;
	display: none;
}
#kumonosu-language-dialog[open]::backdrop {
	background-color: rgba(0, 0, 0, 0.5);
	backdrop-filter: blur(3px);
}
#kumonosu-language-dialog .kumonosu-btn-dialog-close {
	position: absolute;
	top: 0rem;
	right: 25%;
	width: 40px;
	height: 40px;
	border-radius: 50%;
	background-color: #333;
	font-size: 1.2rem;
	color: white;
	border: none;
	cursor: pointer;
	transition: rotate 300ms ease-in-out;
	z-index: 11;
}
#kumonosu-language-dialog .kumonosu-btn-dialog-close:hover {
	rotate: 90deg;
}
.kumonosu-language-options {
	position: absolute;
	inset: 0;
	margin: auto;
	border-radius: 50%;
	aspect-ratio: 1/1;
}
.kumonosu-language-options>label {
	position: absolute;
	transform: translate(-50%, -50%);
	cursor: pointer;
	font-size: 0.9rem;
	width: 36px;
	height: 36px;
	transition: 300ms ease-in-out;
	display: grid;
	place-content: center;
	border-radius: 50%;
}
.kumonosu-language-options>label.kumonosu-active {
	color: white;
	background: var(--theme-color);
}
.kumonosu-language-options>label:hover {
	scale: 1.2;
	z-index: 2;
}
.kumonosu-language-title {
	position: absolute;
	left: 50%;
	top: 50%;
	transform: translate(-50%, -50%);
	pointer-events: none;
	color: #333;
	font-size: 1rem;
	width: 80%;
	text-align: center;
}
.kumonosu-flag-icon {
	font-size: 1.5rem;
	display: grid;
	place-content: center;
}
.kumonosu-language-options input[type="radio"] {
	position: absolute;
	opacity: 0;
	width: 0;
	height: 0;
}
const languageFlags = [{
		code: 'ar-SA',
		name: 'Arabic',
		flag: '🇸🇦',
		bg: '#FFFFFF',
		mask: '#006C35'
	}, // 白 -> 緑
	{
		code: 'cs-CZ',
		name: 'Czech',
		flag: '🇨🇿',
		bg: '#FFFFFF',
		mask: '#D7141A'
	}, {
		code: 'da-DK',
		name: 'Danish',
		flag: '🇩🇰',
		bg: '#FFFFFF',
		mask: '#C60C30'
	}, // 白 -> 赤
	{
		code: 'de-DE',
		name: 'German',
		flag: '🇩🇪',
		bg: '#000000',
		mask: '#FFCE00'
	}, {
		code: 'el-GR',
		name: 'Greek',
		flag: '🇬🇷',
		bg: '#FFFFFF',
		mask: '#005BAE'
	}, // 白 -> 青
	{
		code: 'en-US',
		name: 'English (US)',
		flag: '🇺🇸',
		bg: '#3C3B6E',
		mask: '#B22234'
	}, {
		code: 'en-GB',
		name: 'English (UK)',
		flag: '🇬🇧',
		bg: '#012169',
		mask: '#C8102E'
	}, {
		code: 'es-ES',
		name: 'Spanish',
		flag: '🇪🇸',
		bg: '#C60B1E',
		mask: '#FFD700'
	}, {
		code: 'es-MX',
		name: 'Spanish (MX)',
		flag: '🇲🇽',
		bg: '#006847',
		mask: '#CE1126'
	}, {
		code: 'fi-FI',
		name: 'Finnish',
		flag: '🇫🇮',
		bg: '#FFFFFF',
		mask: '#003580'
	}, {
		code: 'fr-CA',
		name: 'French (CA)',
		flag: '🇨🇦',
		bg: '#FFFFFF',
		mask: '#FF0000'
	}, // 白 -> 赤
	{
		code: 'fr-FR',
		name: 'French (FR)',
		flag: '🇫🇷',
		bg: '#002395',
		mask: '#ED2939'
	}, {
		code: 'he-IL',
		name: 'Hebrew',
		flag: '🇮🇱',
		bg: '#FFFFFF',
		mask: '#0038B8'
	}, {
		code: 'hi-IN',
		name: 'Hindi',
		flag: '🇮🇳',
		bg: '#FF9933',
		mask: '#138808'
	}, {
		code: 'hu-HU',
		name: 'Hungarian',
		flag: '🇭🇺',
		bg: '#436F4D',
		mask: '#CE1126'
	}, // 白 -> 赤
	{
		code: 'it-IT',
		name: 'Italian',
		flag: '🇮🇹',
		bg: '#009246',
		mask: '#CD212A'
	}, {
		code: 'ja-JP',
		name: 'Japanese',
		flag: '🇯🇵',
		bg: '#FFFFFF',
		mask: '#b30000'
	}, {
		code: 'ko-KR',
		name: 'Korean',
		flag: '🇰🇷',
		bg: '#FFFFFF',
		mask: '#CD2E3A'
	}, {
		code: 'nl-NL',
		name: 'Dutch',
		flag: '🇳🇱',
		bg: '#AE1C28',
		mask: '#21468B'
	}, {
		code: 'no-NO',
		name: 'Norwegian',
		flag: '🇳🇴',
		bg: '#BA0C2F',
		mask: '#00205B'
	}, {
		code: 'pl-PL',
		name: 'Polish',
		flag: '🇵🇱',
		bg: '#FFFFFF',
		mask: '#DC143C'
	}, {
		code: 'pt-BR',
		name: 'Portuguese (BR)',
		flag: '🇧🇷',
		bg: '#009739',
		mask: '#FEDF00'
	}, {
		code: 'pt-PT',
		name: 'Portuguese (PT)',
		flag: '🇵🇹',
		bg: '#FF0000',
		mask: '#006600'
	}, {
		code: 'ro-RO',
		name: 'Romanian',
		flag: '🇷🇴',
		bg: '#002B7F',
		mask: '#FCD116'
	}, {
		code: 'ru-RU',
		name: 'Russian',
		flag: '🇷🇺',
		bg: '#FFFFFF',
		mask: '#DA2128'
	}, {
		code: 'sv-SE',
		name: 'Swedish',
		flag: '🇸🇪',
		bg: '#006AA7',
		mask: '#FECC00'
	}, {
		code: 'th-TH',
		name: 'Thai',
		flag: '🇹🇭',
		bg: '#2D2A4A',
		mask: '#A51931'
	}, // 白 -> 赤
	{
		code: 'tr-TR',
		name: 'Turkish',
		flag: '🇹🇷',
		bg: '#FFFFFF',
		mask: '#E30A17'
	}, // 白 -> 赤
	{
		code: 'vi-VN',
		name: 'Vietnamese',
		flag: '🇻🇳',
		bg: '#DA251D',
		mask: '#FFFF00'
	}, {
		code: 'zh-CN',
		name: 'Chinese',
		flag: '🇨🇳',
		bg: '#EE1C25',
		mask: '#FFFF00'
	},
];
const RADIUS_LANG = 140;
const defaultRegions = languageFlags.reduce((map, lang) => {
	const baseLang = lang.code.split('-')[0];
	if (!map[baseLang]) map[baseLang] = lang.code;
	return map;
}, {});

function getLocale() {
	let language = (navigator.languages && navigator.languages[0]) || navigator.language || 'ja-JP';
	if (language.length === 2) {
		language = defaultRegions[language] || `${language}-${language.toUpperCase()}`;
	}
	return language;
}
let locale = getLocale();
const currentLangDisplay = document.getElementById('kumonosu-current-lang');
const languageDialog = document.getElementById('kumonosu-language-dialog');
const languageOptionsContainer = document.getElementById('kumonosu-language-options');
const closeButton = document.getElementById('kumonosu-btn-dialog-close');

function drawClockFaces() {
	const clockFaces = document.querySelectorAll('.kumonosu-clock-face');
	const currentDate = new Date();
	const currentDay = currentDate.getDate();
	const currentMonth = currentDate.getMonth();
	const currentYear = currentDate.getFullYear();
	const currentWeekday = currentDate.getDay();
	const currentHours = currentDate.getHours();
	const currentMinutes = currentDate.getMinutes();
	const currentSeconds = currentDate.getSeconds();
	const totalDaysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
	const weekdayNames = Array.from({
		length: 7
	}, (_, i) => new Intl.DateTimeFormat(locale, {
		weekday: 'long'
	}).format(new Date(2021, 0, i + 3)));
	const monthNames = Array.from({
		length: 12
	}, (_, i) => new Intl.DateTimeFormat(locale, {
		month: 'long'
	}).format(new Date(2021, i)));
	clockFaces.forEach(clockFace => {
		clockFace.innerHTML = '';
		const clockType = clockFace.getAttribute('data-clock');
		const numbers = parseInt(clockFace.getAttribute('data-numbers'), 10);
		const RADIUS = (clockFace.offsetWidth / 2) - 20;
		const center = clockFace.offsetWidth / 2;
		let valueSet;
		let currentValue;
		switch (clockType) {
			case 'seconds':
				valueSet = Array.from({
					length: 60
				}, (_, i) => String(i).padStart(2, '0'));
				currentValue = String(currentSeconds).padStart(2, '0');
				break;
			case 'minutes':
				valueSet = Array.from({
					length: 60
				}, (_, i) => String(i).padStart(2, '0'));
				currentValue = String(currentMinutes).padStart(2, '0');
				break;
			case 'hours':
				valueSet = Array.from({
					length: 24
				}, (_, i) => String(i).padStart(2, '0'));
				currentValue = String(currentHours).padStart(2, '0');
				break;
			case 'days':
				valueSet = Array.from({
					length: totalDaysInMonth
				}, (_, i) => i + 1);
				currentValue = currentDay;
				break;
			case 'months':
				valueSet = monthNames;
				currentValue = currentMonth;
				break;
			case 'years':
				valueSet = Array.from({
					length: 101
				}, (_, i) => 2000 + i);
				currentValue = currentYear;
				break;
			case 'day-names':
				valueSet = weekdayNames;
				currentValue = currentWeekday;
				break;
			default:
				return;
		}
		valueSet.forEach((value, i) => {
			const angle = (i * (360 / numbers));
			const x = center + RADIUS * Math.cos((angle * Math.PI) / 180);
			const y = center + RADIUS * Math.sin((angle * Math.PI) / 180);
			const element = document.createElement('span');
			element.classList.add('kumonosu-number');
			element.textContent = value;
			element.style.left = `${x}px`;
			element.style.top = `${y}px`;
			element.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;
			clockFace.appendChild(element);
		});
		const currentIndex = valueSet.indexOf(typeof valueSet[0] === 'string' ? String(currentValue) : currentValue);
		const rotationAngle = -((currentIndex / numbers) * 360);
		clockFace.style.transform = `rotate(${rotationAngle}deg)`;
	});
}
const lastAngles = {};

function rotateClockFaces() {
	const clockFaces = document.querySelectorAll('.kumonosu-clock-face');

	function updateRotations() {
		const now = new Date();
		const currentSecond = now.getSeconds();
		const currentMinute = now.getMinutes();
		const currentHour = now.getHours();
		const currentDay = now.getDate();
		const currentMonth = now.getMonth();
		const currentYear = now.getFullYear();
		const currentWeekday = now.getDay();
		clockFaces.forEach(clockFace => {
			const clockType = clockFace.getAttribute('data-clock');
			const totalNumbers = parseInt(clockFace.getAttribute('data-numbers'), 10);
			let currentValue;
			switch (clockType) {
				case 'seconds':
					currentValue = currentSecond;
					break;
				case 'minutes':
					currentValue = currentMinute;
					break;
				case 'hours':
					currentValue = currentHour;
					break;
				case 'days':
					currentValue = currentDay - 1;
					break;
				case 'months':
					currentValue = currentMonth;
					break;
				case 'years':
					currentValue = currentYear - 2000;
					break;
				case 'day-names':
					currentValue = currentWeekday;
					break;
				default:
					return;
			}
			const targetAngle = (360 / totalNumbers) * currentValue;
			const clockId = clockFace.id || clockType;
			const lastAngle = lastAngles[clockId] || 0;
			const delta = targetAngle - lastAngle;
			const shortestDelta = ((delta + 540) % 360) - 180;
			const newAngle = lastAngle + shortestDelta;
			clockFace.style.transform = `rotate(${newAngle * -1}deg)`;
			lastAngles[clockId] = newAngle;
			const numbers = clockFace.querySelectorAll('.kumonosu-number');
			numbers.forEach((number, index) => {
				if (index === currentValue) number.classList.add('kumonosu-active');
				else number.classList.remove('kumonosu-active');
			});
		});
		requestAnimationFrame(updateRotations);
	}
	updateRotations();
}

function createLanguageOptions() {
	const centerX = languageOptionsContainer.offsetWidth / 2;
	const centerY = languageOptionsContainer.offsetHeight / 2;
	languageOptionsContainer.innerHTML = '';
	languageFlags.forEach((lang, index, arr) => {
		const angle = (index / arr.length) * 2 * Math.PI;
		const x = centerX + RADIUS_LANG * Math.cos(angle);
		const y = centerY + RADIUS_LANG * Math.sin(angle);
		const radioWrapper = document.createElement('label');
		radioWrapper.title = lang.name;
		radioWrapper.style.left = `${x}px`;
		radioWrapper.style.top = `${y}px`;
		const radioInput = document.createElement('input');
		radioInput.type = 'radio';
		radioInput.name = 'language';
		radioInput.value = lang.code;
		if (lang.code === locale) {
			radioInput.checked = true;
			radioWrapper.classList.add('kumonosu-active');
		}
		const flag = document.createElement('span');
		flag.classList.add('kumonosu-flag-icon');
		flag.innerText = lang.flag;
		radioWrapper.appendChild(radioInput);
		radioWrapper.appendChild(flag);
		languageOptionsContainer.appendChild(radioWrapper);
		radioWrapper.addEventListener('mouseover', () => showTitle(lang.name));
		radioWrapper.addEventListener('mouseleave', () => hideTitle());
		radioInput.addEventListener('change', () => {
			locale = radioInput.value;
			setCurrentLangDisplay(lang);
			drawClockFaces();
			document.querySelector('label.kumonosu-active')?.classList.remove('kumonosu-active');
			radioWrapper.classList.add('kumonosu-active');
			closeDialog();
		});
	});
}
let titleDisplay = null;

function showTitle(languageName) {
	if (!titleDisplay) {
		titleDisplay = document.createElement('div');
		titleDisplay.classList.add('kumonosu-language-title');
		languageOptionsContainer.appendChild(titleDisplay);
	}
	titleDisplay.textContent = languageName;
}

function hideTitle() {
	if (titleDisplay) titleDisplay.textContent = '';
}

function getContrastColor(hexcolor) {
	if (!hexcolor || hexcolor === 'transparent') return '#000000';
	const hex = hexcolor.replace('#', '');
	const r = parseInt(hex.substr(0, 2), 16);
	const g = parseInt(hex.substr(2, 2), 16);
	const b = parseInt(hex.substr(4, 2), 16);
	const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
	return (yiq >= 128) ? '#000000' : '#FFFFFF';
}

function setCurrentLangDisplay(lang) {
	if (lang) {
		currentLangDisplay.textContent = lang.flag;
		currentLangDisplay.title = lang.name;
		document.documentElement.style.setProperty('--bg-clr', lang.bg);
		document.documentElement.style.setProperty('--theme-color', lang.mask);
		const normalTextClr = getContrastColor(lang.bg);
		document.documentElement.style.setProperty('--clock-numbers-clr', normalTextClr);
		let activeTextClr;
		if (normalTextClr === '#000000') {
			activeTextClr = '#000000';
		} else {
			activeTextClr = getContrastColor(lang.mask);
		}
		document.documentElement.style.setProperty('--active-numbers-clr', activeTextClr);
	}
}

function openDialog() {
	languageDialog.showModal();
	createLanguageOptions();
	languageDialog.addEventListener('click', closeDialogOnClickOutside);
}

function closeDialog() {
	languageDialog.close();
	languageDialog.removeEventListener('click', closeDialogOnClickOutside);
}

function closeDialogOnClickOutside(e) {
	if (e.target === languageDialog) closeDialog();
}
closeButton.addEventListener('click', closeDialog);
currentLangDisplay.addEventListener('click', openDialog);
// Initialize
drawClockFaces();
rotateClockFaces();
setCurrentLangDisplay(languageFlags.find(lang => lang.code === locale) || languageFlags.find(l => l.code === 'ja-JP'));

Explanation 詳しい説明

どんな時計?(コンセプト)

この作品は、世界時計を「都市名のリスト」ではなく、円形のリングが回転するビジュアルとして表現したUIです。
時間の各要素(秒・分・時・日・月・年・曜日)をそれぞれ別レイヤーに分け、現在値に合わせてリング全体が回転します。

数字が切り替わるのではなく“リングが動く”ため、眺めているだけで時間の流れが感じられるのが特徴です。

仕様(動きと構造)

  • リングは7層(years / seconds / minutes / hours / days / months / day-names)
  • 各リングの中身(数値・月名・曜日名)は、drawClockFaces() で円周上に並べて生成
  • 現在値に応じた回転は rotateClockFaces() で常時更新
    requestAnimationFrame() で滑らかに追従
    ・回転角は「最短ルート(shortestDelta)」を計算して、逆回転で不自然に戻るのを防止
  • 言語とタイムゾーンは国旗ボタン → <dialog> で選択
    ・言語:曜日・月名の表示に反映(Intl.DateTimeFormat(currentLocale)
    ・タイムゾーン:現在時刻の取得に反映(getTimeInZone(tz)

世界の時間を切り替えられる仕組み

この時計の“世界時計らしさ”は、ここが肝です。

  • languageFlagslocale(code)と timeZone(tz) をセット
  • 選択された currentTimeZone を使って、Intl.DateTimeFormatformatToParts() から
    年/月/日/時/分/秒/曜日 をパーツとして取り出し、表示用の値に変換しています

つまり、端末の現在時刻をベースに「指定タイムゾーンの現在」を組み立て直すことで、各国の“いま”を実現しています。

デザイン面(CSSの見どころ)

  • --clock-size を基準にして、リングを段階的に小さくする設計
  • 中央の赤い帯は clip-path で作ったマスク表現
  • 背景色・アクセント色はCSS変数で統一管理し、国選択で一括変更
  • 回転の気持ちよさはここ
    .kumonosu-clock-face { transition: 500ms cubic-bezier(...) }
    ・リングが「スッ」と追従する感覚が出ます

カスタム方法

1)時計サイズを変える

:root { --clock-size: 800px; }

大きいほど余白が取れて読みやすく、小さいほどUI感が強くなります。

2)リングの文字サイズを調整

.kumonosu-clock > div { font-size: 0.7rem; }

年月リングは情報量が多いので、ここを少し下げるのも手です。

3)国(言語・タイムゾーン)を追加する

languageFlags に1行追加するだけでOKです。

{ code: 'en-AU', name: 'Australia', flag: '🇦🇺', bg: '#001B5A', mask: '#FFB300', tz: 'Australia/Sydney' }

4)配色をもっと“国っぽく”する

bgmask の2色で雰囲気が激変します。
背景が暗い国は、文字色が自動で白寄りになるよう getContrastColor() が効きます。

注意点(落とし穴)

  • Intl.DateTimeFormattimeZone は環境依存があります
    ほとんどの現行ブラウザは問題ないですが、古い環境では対応が弱い場合があります。
  • リングの要素数が多い(特に年101・秒60など)ため、端末によっては負荷が出ることがあります
    → 対策例:年リングを 51 に減らす、requestAnimationFramesetInterval(250ms) にする など
  • <dialog> 未対応ブラウザ向けには、モーダルをdivで作るフォールバックが必要です