0 of 5 syllables
0 of 7 syllables
0 of 5 syllables
import { useId, useState } from 'react';
const TICKS = Array.from({ length: 12 }, (_, i) => i);
// "930" → "09:30", "1745" → "17:45": digits only, the colon goes in by itself.
function mask(raw: string) {
const d = raw.replace(/\D/g, '').slice(0, 4);
return d.length > 2 ? `${d.slice(0, 2)}:${d.slice(2)}` : d;
}
const css = `
.field-dial-time { display: flex; align-items: center; gap: 18px; width: 260px; }
.field-dial-time__clock { position: relative; flex: none; width: 84px; height: 84px; border-radius: 50%; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#cbced3, #393c41), 0 10px 20px -12px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7)); }
.field-dial-time__tick { position: absolute; left: 50%; top: 5px; width: 1.5px; height: 5px; margin-left: -0.75px; background: light-dark(#6d7179, #7d8087); transform-origin: 50% 37px; transform: rotate(calc(var(--k) * 30deg)); }
.field-dial-time__tick:nth-child(3n + 1) { height: 8px; width: 2px; background: light-dark(#50535b, #c6c8cd); }
.field-dial-time__hand { position: absolute; left: 50%; bottom: 50%; border-radius: 2px; transform-origin: 50% 100%; transition: transform 0.9s cubic-bezier(0.3, 1.25, 0.4, 1); }
.field-dial-time__hand--h { width: 3px; height: 22px; margin-left: -1.5px; background: light-dark(#373a40, #f1f2f3); transform: rotate(var(--h)); }
.field-dial-time__hand--m { width: 2px; height: 32px; margin-left: -1px; background: light-dark(#50535b, #c6c8cd); transform: rotate(var(--m)); }
.field-dial-time__pin { position: absolute; left: 50%; top: 50%; width: 7px; height: 7px; margin: -3.5px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 0 2px light-dark(#f3f4f7, #1a1c1f); }
.field-dial-time__field { flex: 1; min-width: 0; }
.field-dial-time__label { display: block; margin-bottom: 6px; color: light-dark(#50535b, #c6c8cd); font-size: 12px; }
.field-dial-time__input { width: 100%; height: 44px; padding: 0 12px; border: 0; border-radius: 10px; outline: 0; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); color: light-dark(#373a40, #f1f2f3); font: 500 22px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.06em; transition: box-shadow 0.2s; }
.field-dial-time__input:focus { box-shadow: inset 0 0 0 1px light-dark(#5f626a, #a7abb2), 0 0 0 3px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent); }
.field-dial-time__input::placeholder { color: light-dark(#cbced3, #393c41); }
.field-dial-time__input[aria-invalid='true'] { box-shadow: inset 0 0 0 1px light-dark(#50535b, #c6c8cd); }
.field-dial-time__part { display: block; margin-top: 6px; color: light-dark(#5f626a, #a7abb2); font-size: 12px; }
@media (prefers-reduced-motion: reduce) {
.field-dial-time__hand { transition: none; }
}
`;
export function DialTimeInput() {
const id = useId();
const [value, setValue] = useState('');
const m = value.match(/^(\d{2}):(\d{2})$/);
const h = m ? +m[1]! : 10;
const min = m ? +m[2]! : 10;
const valid = !m || (h < 24 && min < 60);
const ok = m && valid;
const part = !ok ? 'Type a 24-hour time' : h < 5 ? 'Small hours' : h < 12 ? 'Morning' : h < 17 ? 'Afternoon' : h < 21 ? 'Evening' : 'Night';
return (
<>
<style>
{css}
</style>
<div className="field-dial-time">
<span
className="field-dial-time__clock"
aria-hidden="true"
style={{ '--h': `${((h % 12) + min / 60) * 30}deg`, '--m': `${min * 6}deg` } as React.CSSProperties}
>
{TICKS.map((k) => (
<span key={k} className="field-dial-time__tick" style={{ '--k': k } as React.CSSProperties} />
))}
<span className="field-dial-time__hand field-dial-time__hand--h" />
<span className="field-dial-time__hand field-dial-time__hand--m" />
<span className="field-dial-time__pin" />
</span>
<div className="field-dial-time__field">
<label className="field-dial-time__label" htmlFor={id}>
Pickup time
</label>
<input
id={id}
className="field-dial-time__input"
inputMode="numeric"
placeholder="09:30"
value={value}
aria-invalid={!valid}
aria-describedby={`${id}-part`}
onChange={(e) => setValue(mask(e.target.value))}
/>
<span id={`${id}-part`} className="field-dial-time__part" aria-live="polite">
{valid ? part : 'That time does not exist'}
</span>
</div>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initFieldDialTime(el) on the mounted element instead.
<div class="field-dial-time">
<span class="field-dial-time__clock" aria-hidden="true" style="--h:305deg;--m:60deg">
<span class="field-dial-time__tick" style="--k:0"></span>
<span class="field-dial-time__tick" style="--k:1"></span>
<span class="field-dial-time__tick" style="--k:2"></span>
<span class="field-dial-time__tick" style="--k:3"></span>
<span class="field-dial-time__tick" style="--k:4"></span>
<span class="field-dial-time__tick" style="--k:5"></span>
<span class="field-dial-time__tick" style="--k:6"></span>
<span class="field-dial-time__tick" style="--k:7"></span>
<span class="field-dial-time__tick" style="--k:8"></span>
<span class="field-dial-time__tick" style="--k:9"></span>
<span class="field-dial-time__tick" style="--k:10"></span>
<span class="field-dial-time__tick" style="--k:11"></span>
<span class="field-dial-time__hand field-dial-time__hand--h"></span>
<span class="field-dial-time__hand field-dial-time__hand--m"></span>
<span class="field-dial-time__pin"></span>
</span>
<div class="field-dial-time__field">
<label class="field-dial-time__label" for="field-dial-time">Pickup time</label>
<input id="field-dial-time" class="field-dial-time__input" inputMode="numeric" placeholder="09:30" aria-invalid="false" aria-describedby="field-dial-time-part" value=""/>
<span id="field-dial-time-part" class="field-dial-time__part" aria-live="polite">Type a 24-hour time</span>
</div>
</div>.field-dial-time { display: flex; align-items: center; gap: 18px; width: 260px; }
.field-dial-time__clock { position: relative; flex: none; width: 84px; height: 84px; border-radius: 50%; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#cbced3, #393c41), 0 10px 20px -12px light-dark(rgba(0, 0, 0, 0.21), rgba(0, 0, 0, 0.7)); }
.field-dial-time__tick { position: absolute; left: 50%; top: 5px; width: 1.5px; height: 5px; margin-left: -0.75px; background: light-dark(#6d7179, #7d8087); transform-origin: 50% 37px; transform: rotate(calc(var(--k) * 30deg)); }
.field-dial-time__tick:nth-child(3n + 1) { height: 8px; width: 2px; background: light-dark(#50535b, #c6c8cd); }
.field-dial-time__hand { position: absolute; left: 50%; bottom: 50%; border-radius: 2px; transform-origin: 50% 100%; transition: transform 0.9s cubic-bezier(0.3, 1.25, 0.4, 1); }
.field-dial-time__hand--h { width: 3px; height: 22px; margin-left: -1.5px; background: light-dark(#373a40, #f1f2f3); transform: rotate(var(--h)); }
.field-dial-time__hand--m { width: 2px; height: 32px; margin-left: -1px; background: light-dark(#50535b, #c6c8cd); transform: rotate(var(--m)); }
.field-dial-time__pin { position: absolute; left: 50%; top: 50%; width: 7px; height: 7px; margin: -3.5px; border-radius: 50%; background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 0 2px light-dark(#f3f4f7, #1a1c1f); }
.field-dial-time__field { flex: 1; min-width: 0; }
.field-dial-time__label { display: block; margin-bottom: 6px; color: light-dark(#50535b, #c6c8cd); font-size: 12px; }
.field-dial-time__input { width: 100%; height: 44px; padding: 0 12px; border: 0; border-radius: 10px; outline: 0; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); color: light-dark(#373a40, #f1f2f3); font: 500 22px/1 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.06em; transition: box-shadow 0.2s; }
.field-dial-time__input:focus { box-shadow: inset 0 0 0 1px light-dark(#5f626a, #a7abb2), 0 0 0 3px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent); }
.field-dial-time__input::placeholder { color: light-dark(#cbced3, #393c41); }
.field-dial-time__input[aria-invalid='true'] { box-shadow: inset 0 0 0 1px light-dark(#50535b, #c6c8cd); }
.field-dial-time__part { display: block; margin-top: 6px; color: light-dark(#5f626a, #a7abb2); font-size: 12px; }
@media (prefers-reduced-motion: reduce) {
.field-dial-time__hand { transition: none; }
}function initFieldDialTime(root) {
var input = root.querySelector('.field-dial-time__input');
var clock = root.querySelector('.field-dial-time__clock');
var part = root.querySelector('.field-dial-time__part');
// "930" → "09:30", "1745" → "17:45": digits only, the colon goes in by itself.
function mask(raw) {
var d = raw.replace(/\D/g, '').slice(0, 4);
return d.length > 2 ? d.slice(0, 2) + ':' + d.slice(2) : d;
}
function update() {
input.value = mask(input.value);
var m = input.value.match(/^(\d{2}):(\d{2})$/);
var h = m ? +m[1] : 10;
var min = m ? +m[2] : 10;
var valid = !m || (h < 24 && min < 60);
var ok = m && valid;
var name = !ok ? 'Type a 24-hour time' : h < 5 ? 'Small hours' : h < 12 ? 'Morning' : h < 17 ? 'Afternoon' : h < 21 ? 'Evening' : 'Night';
clock.style.setProperty('--h', ((h % 12) + min / 60) * 30 + 'deg');
clock.style.setProperty('--m', min * 6 + 'deg');
input.setAttribute('aria-invalid', String(!valid));
part.textContent = valid ? name : 'That time does not exist';
}
input.addEventListener('input', update);
return function () { input.removeEventListener('input', update); };
}
document.querySelectorAll('.field-dial-time').forEach(initFieldDialTime);Goal: Create a "Dial time" input.
Visual behavior: A pickup time field: type a 24-hour time and the clock beside it sweeps its hands round to the time with a little overshoot, and says whether it is morning, afternoon or night.
Technique:
- Digits only: a mask adds the colon after the hours as you type.
- Once the time is complete, the hour and minute hands are set from it as two rotation custom properties and transition round on an overshooting curve.
- A line under the field names the part of the day, or says the time does not exist (for 25:00), with aria-invalid set to match.
Accessibility: A real <input> with a visible <label>; decoration is aria-hidden. The part of the day is aria-live and linked by aria-describedby, and an impossible time sets aria-invalid. Under prefers-reduced-motion the hands jump to the time.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Dial time" input.
Visual behavior: A pickup time field: type a 24-hour time and the clock beside it sweeps its hands round to the time with a little overshoot, and says whether it is morning, afternoon or night.
Technique:
- Digits only: a mask adds the colon after the hours as you type.
- Once the time is complete, the hour and minute hands are set from it as two rotation custom properties and transition round on an overshooting curve.
- A line under the field names the part of the day, or says the time does not exist (for 25:00), with aria-invalid set to match.
Accessibility: A real <input> with a visible <label>; decoration is aria-hidden. The part of the day is aria-live and linked by aria-describedby, and an impossible time sets aria-invalid. Under prefers-reduced-motion the hands jump to the time.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
A pickup time field: type a 24-hour time and the clock beside it sweeps its hands round to the time with a little overshoot, and says whether it is morning, afternoon or night.
A real <input> with a visible <label>; decoration is aria-hidden. The part of the day is aria-live and linked by aria-describedby, and an impossible time sets aria-invalid. Under prefers-reduced-motion the hands jump to the time.