06Estimated-time unit switch — the unit of a displayed time-estimate value switches each time a computed bit value passes a boundary value
Each time the computed bit value passes one of a few set boundary values, the unit of the displayed time-estimate value switches.
Best forAn account settings screen that builds security awareness, a secret-value issuance tool aimed at developers
GUESSES_PER_SEC = 1e6guessesOf(value)UNITS 단위 배열

<main class="stage">
<div class="ct is-demo" id="ct">
<label class="ct__field">
<input class="ct__input" id="ctInput" type="text" autocomplete="off" spellcheck="false" value="">
</label>
<div class="ct__readout">
<span class="ct__time" id="ctTime">Instant</span>
</div>
<div class="ct__track"><div class="ct__fill" id="ctFill"></div></div>
<div class="ct__meta">Possible combinations <b id="ctGuesses">0</b></div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 경우의 수(문자 집합 크기^글자 수) ÷ 가정 초당 시도 수(GUESSES_PER_SEC,
// 데모 안 임의 상수)로 초 값을 만들고, log10 스케일로 막대를 채운다.
@include stage($stage-yellow);
.ct {
width: 100%;
max-width: 320px;
display: grid;
gap: $sp-3;
}
.ct__field { display: block; }
.ct__input {
width: 100%;
box-sizing: border-box;
padding: $sp-3 $sp-4;
border-radius: $r-control;
border: 0;
background: rgba($stage-ink, .08);
color: $stage-ink;
font: 500 16px/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
letter-spacing: .04em;
-webkit-text-security: disc;
&:focus { outline: 2px solid $stage-ink; outline-offset: 2px; }
}
.ct__readout {
display: flex;
justify-content: center;
}
.ct__time {
font: 800 26px/1.1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $stage-ink;
font-variant-numeric: tabular-nums;
}
.ct__track {
position: relative;
height: $sp-3;
border-radius: $r-sm;
background: rgba($stage-ink, .1);
overflow: hidden;
}
.ct__fill {
position: absolute;
inset: 0;
border-radius: $r-sm;
background: $stage-ink;
transform: scaleX(0);
transform-origin: left center;
transition: transform $dur-base $ease-out;
}
.ct__meta {
font: 600 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: rgba($stage-ink, .72);
text-align: center;
b { color: $stage-ink; font-variant-numeric: tabular-nums; }
}
@media (max-width: 340px) {
.ct { max-width: 260px; }
.ct__input { padding: $sp-2 $sp-3; font-size: 14px; }
.ct__time { font-size: 21px; }
.ct__meta { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.ct__fill { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.ct {
width: 100%;
max-width: 320px;
display: grid;
gap: 12px;
}
.ct__field {
display: block;
}
.ct__input {
width: 100%;
box-sizing: border-box;
padding: 12px 16px;
border-radius: 12px;
border: 0;
background: rgba(23, 20, 26, 0.08);
color: #17141a;
font: 500 16px/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
letter-spacing: 0.04em;
-webkit-text-security: disc;
}
.ct__input:focus {
outline: 2px solid #17141a;
outline-offset: 2px;
}
.ct__readout {
display: flex;
justify-content: center;
}
.ct__time {
font: 800 26px/1.1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #17141a;
font-variant-numeric: tabular-nums;
}
.ct__track {
position: relative;
height: 12px;
border-radius: 8px;
background: rgba(23, 20, 26, 0.1);
overflow: hidden;
}
.ct__fill {
position: absolute;
inset: 0;
border-radius: 8px;
background: #17141a;
transform: scaleX(0);
transform-origin: left center;
transition: transform 300ms ease-out;
}
.ct__meta {
font: 600 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: rgba(23, 20, 26, 0.72);
text-align: center;
}
.ct__meta b {
color: #17141a;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.ct {
max-width: 260px;
}
.ct__input {
padding: 8px 12px;
font-size: 14px;
}
.ct__time {
font-size: 21px;
}
.ct__meta {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.ct__fill {
transition: none;
}
}(function () {
var root = document.getElementById('ct');
var input = document.getElementById('ctInput');
var timeEl = document.getElementById('ctTime');
var fill = document.getElementById('ctFill');
var guessesEl = document.getElementById('ctGuesses');
// 가정값: 데모 안 임의 상수 — 실제 하드웨어·연도·연구 인용이 아니다.
var GUESSES_PER_SEC = 1e6;
var LOG_MAX = 12; // 막대 눈금 상한(sec 단위 log10) — 이 Value 이상은 꽉 찬 막대
function charsetSize(value) {
var size = 0;
if (/[a-z]/.test(value)) size += 26;
if (/[A-Z]/.test(value)) size += 26;
if (/[0-9]/.test(value)) size += 10;
if (/[^A-Za-z0-9]/.test(value)) size += 32;
return size;
}
// 계산 겹: Char set 크기의 Length 제곱으로 경우의 수를 구하고, 가정 초당
// 시도 수로 나눠 sec 값을 만든 뒤, 자리 단위(sec→min→hr→day→yr→century)로 변환한다.
function guessesOf(value) {
var size = charsetSize(value);
if (!value.length || size === 0) return 0;
return Math.pow(size, value.length);
}
var UNITS = [
{ at: 1, name: 'sec' },
{ at: 60, name: 'min', div: 60 },
{ at: 3600, name: 'hr', div: 3600 },
{ at: 86400, name: 'day', div: 86400 },
{ at: 31536000, name: 'yr', div: 31536000 },
{ at: 31536000 * 100, name: 'century', div: 31536000 * 100 }
];
function formatTime(seconds) {
if (seconds < 1) return 'Instant';
var pick = UNITS[0];
for (var i = 0; i < UNITS.length; i++) if (seconds >= UNITS[i].at) pick = UNITS[i];
var div = pick.div || 1;
var n = seconds / div;
var text = n >= 100 ? Math.round(n).toLocaleString() : (Math.round(n * 10) / 10).toString();
return 'about ' + text + pick.name;
}
function paint(value) {
var guesses = guessesOf(value);
var seconds = guesses / GUESSES_PER_SEC;
timeEl.textContent = formatTime(seconds);
var mag = seconds < 1 ? 0 : Math.log(seconds + 1) / Math.LN10;
fill.style.transform = 'scaleX(' + Math.max(0, Math.min(1, mag / LOG_MAX)) + ')';
guessesEl.textContent = guesses >= 1e6 ? guesses.toExponential(1) : Math.round(guesses).toLocaleString();
}
input.addEventListener('input', function () {
root.classList.remove('is-demo');
stopAutoplay();
paint(input.value);
});
paint(input.value);
// 자동 루프: 시드 고정 문자열 배열로 길이·문자 종류를 함께 늘렸다 줄인다.
var SEQ = ['', 'ab', 'abcd12', 'abcd12EF', 'abcd12EF!@', 'abcd12EF!@#$xy',
'abcd12EF!@', 'abcd12EF', 'abcd12', 'ab', ''];
var si = 0;
var autoTimer = null;
function autoTick() { input.value = SEQ[si]; paint(SEQ[si]); si = (si + 1) % SEQ.length; }
function stopAutoplay() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) { autoTimer = setInterval(autoTick, 380); autoTick(); }
})();import { useEffect, useRef } from "react";
import styles from "./CrackTimeEstimate.module.scss";
type Box<T> = { current: T };
// 가정값: 데모 안 임의 상수 — 실제 하드웨어·연도·연구 인용이 아니다.
const GUESSES_PER_SEC = 1e6;
const LOG_MAX = 12;
type Unit = { at: number; name: string; div?: number };
const UNITS: Unit[] = [
{ at: 1, name: "초" },
{ at: 60, name: "분", div: 60 },
{ at: 3600, name: "시간", div: 3600 },
{ at: 86400, name: "일", div: 86400 },
{ at: 31536000, name: "년", div: 31536000 },
{ at: 31536000 * 100, name: "세기", div: 31536000 * 100 },
];
export default function CrackTimeEstimate() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const input = root.querySelector(`.${styles.input}`) as HTMLInputElement | null;
const timeEl = root.querySelector(`.${styles.time}`) as HTMLElement | null;
const fill = root.querySelector(`.${styles.fill}`) as HTMLElement | null;
const guessesEl = root.querySelector(`.${styles.guesses}`) as HTMLElement | null;
if (!input || !timeEl || !fill || !guessesEl) return;
function charsetSize(value: string) {
let size = 0;
if (/[a-z]/.test(value)) size += 26;
if (/[A-Z]/.test(value)) size += 26;
if (/[0-9]/.test(value)) size += 10;
if (/[^A-Za-z0-9]/.test(value)) size += 32;
return size;
}
// 계산 겹: 문자 집합 크기의 글자 수 제곱으로 경우의 수를 구하고, 가정 초당
// 시도 수로 나눠 초 값을 만든 뒤, 자리 단위(초→분→시간→일→년→세기)로 변환한다.
function guessesOf(value: string) {
const size = charsetSize(value);
if (!value.length || size === 0) return 0;
return Math.pow(size, value.length);
}
function formatTime(seconds: number) {
if (seconds < 1) return "즉시";
let pick = UNITS[0];
for (let i = 0; i < UNITS.length; i++) if (seconds >= UNITS[i].at) pick = UNITS[i];
const div = pick.div || 1;
const n = seconds / div;
const text = n >= 100 ? Math.round(n).toLocaleString() : (Math.round(n * 10) / 10).toString();
return "약 " + text + pick.name;
}
function paint(value: string) {
const guesses = guessesOf(value);
const seconds = guesses / GUESSES_PER_SEC;
timeEl!.textContent = formatTime(seconds);
const mag = seconds < 1 ? 0 : Math.log(seconds + 1) / Math.LN10;
fill!.style.transform = "scaleX(" + Math.max(0, Math.min(1, mag / LOG_MAX)) + ")";
guessesEl!.textContent = guesses >= 1e6 ? guesses.toExponential(1) : Math.round(guesses).toLocaleString();
}
function onInput() {
root!.classList.remove(styles["is-demo"]);
stopAutoplay();
paint(input!.value);
}
input.addEventListener("input", onInput);
paint(input.value);
// 자동 루프: 시드 고정 문자열 배열로 길이·문자 종류를 함께 늘렸다 줄인다.
const SEQ = ["", "ab", "abcd12", "abcd12EF", "abcd12EF!@", "abcd12EF!@#$xy",
"abcd12EF!@", "abcd12EF", "abcd12", "ab", ""];
let si = 0;
let autoTimer: number | null = null;
function autoTick() { input!.value = SEQ[si]; paint(SEQ[si]); si = (si + 1) % SEQ.length; }
function stopAutoplay() { if (autoTimer !== null) { window.clearInterval(autoTimer); autoTimer = null; } }
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!reduce) { autoTimer = window.setInterval(autoTick, 380); autoTick(); }
return () => {
stopAutoplay();
input.removeEventListener("input", onInput);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.ct} ${styles["is-demo"]}`}>
<label className={styles.field}>
<input className={styles.input} type="text" autoComplete="off" spellCheck={false} defaultValue="" />
</label>
<div className={styles.readout}>
<span className={styles.time}>즉시</span>
</div>
<div className={styles.track}><div className={styles.fill} /></div>
<div className={styles.meta}>경우의 수 <b className={styles.guesses}>0</b></div>
</div>
);
}06 Estimated-time unit switch — the unit of a displayed time-estimate value switches each time a computed bit value passes a boundary value — Measured area 7.119% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · Password Strength Meter UI: 9 Calculation Patterns
The post this demo lives in
Password Strength Meter UI: 9 Calculation PatternsOne strength value is set by how many character types mix in, one drops when the input matches a common value, and one f See all demos in this post
If you like this effect - similar demos
Inputs08 Unit toggle flip — the card folds over and the display value switches at the 90-degree point once the unit enters a changed stateNumber Stepper UI: 9 Interaction Patterns
Inputs05 Live validity value toggle — each time a digit value fills in, the whole number's validity value is recalculated and the display value changesCredit Card Input UI: 9 Patterns
Inputs04 Count-badge roll toggle — each time the selected-count value rises or falls, the badge digit's position value rolls up or downCSS Custom Checkbox: 9 Interaction Patterns