05Repeat/sequence penalty — the strength value drops by an amount tied to a run of the same character or a sequential digit value
Whenever a run of the same character or a sequentially ordered digit value is found, the strength value drops by an amount tied to that run's length value.
Best forAn account security setting that filters out values like 1234 or aaaa, a signup form that detects patterned values
flagPositions(value)PER_CHAR_PENALTY = 8.rs__chip--on

<main class="stage">
<div class="rs is-demo" id="rs">
<label class="rs__field">
<input class="rs__input" id="rsInput" type="text" autocomplete="off" spellcheck="false" value="">
</label>
<div class="rs__chips" id="rsChips"></div>
<div class="rs__track"><div class="rs__fill" id="rsFill"></div></div>
<div class="rs__row">
<span class="rs__score">Value <b id="rsScore">0</b>/100</span>
<span class="rs__warn" id="rsWarn">Repeat/sequence <b id="rsPenalty">-0</b></span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 같은 글자 3연속(반복) 또는 문자코드 증감 1씩 3연속(연속)을 찾아
// 그 글자 수만큼 고정 감점을 기본 점수(길이×8)에서 뺀다.
@include stage($stage-ink);
.rs {
width: 100%;
max-width: 320px;
display: grid;
gap: $sp-3;
}
.rs__field { display: block; }
.rs__input {
width: 100%;
box-sizing: border-box;
padding: $sp-3 $sp-4;
border-radius: $r-control;
border: 0;
background: rgba($stage-paper, .12);
color: $subject-cream;
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 $subject-blue-light; outline-offset: 2px; }
}
.rs__chips {
display: flex;
flex-wrap: wrap;
gap: $sp-1;
min-height: $sp-4;
}
.rs__chip {
width: $sp-3;
height: $sp-3;
border-radius: $r-xs;
background: rgba($stage-paper, .28);
transform: scale(1);
transition: background-color $dur-base $ease-out, transform $dur-base $ease-out;
}
.rs__chip--on {
background: $error;
transform: scale(1.15);
}
.rs__track {
position: relative;
height: $sp-3;
border-radius: $r-sm;
background: rgba($stage-paper, .14);
overflow: hidden;
}
.rs__fill {
position: absolute;
inset: 0;
border-radius: $r-sm;
background: $subject-blue-light;
transform: scaleX(0);
transform-origin: left center;
transition: transform $dur-base $ease-out;
}
.rs--hit .rs__fill { background: $tick-down; }
.rs__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: $sp-2;
font: 600 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $stage-paper;
}
.rs__score b { font-variant-numeric: tabular-nums; }
.rs__warn {
color: $tick-down;
opacity: 0;
transform: scale(.7);
transform-origin: right center;
transition: opacity $dur-base $ease-out, transform $dur-base $ease-out;
}
@media (max-width: 340px) {
.rs { max-width: 260px; }
.rs__input { padding: $sp-2 $sp-3; font-size: 14px; }
.rs__row { font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.rs__fill, .rs__warn, .rs__chip { 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);
}
.rs {
width: 100%;
max-width: 320px;
display: grid;
gap: 12px;
}
.rs__field {
display: block;
}
.rs__input {
width: 100%;
box-sizing: border-box;
padding: 12px 16px;
border-radius: 12px;
border: 0;
background: rgba(255, 255, 255, 0.12);
color: #fff7e6;
font: 500 16px/1.3 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
letter-spacing: 0.04em;
-webkit-text-security: disc;
}
.rs__input:focus {
outline: 2px solid #5b8cf8;
outline-offset: 2px;
}
.rs__chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 16px;
}
.rs__chip {
width: 12px;
height: 12px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.28);
transform: scale(1);
transition: background-color 300ms ease-out, transform 300ms ease-out;
}
.rs__chip--on {
background: #c2361a;
transform: scale(1.15);
}
.rs__track {
position: relative;
height: 12px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.14);
overflow: hidden;
}
.rs__fill {
position: absolute;
inset: 0;
border-radius: 8px;
background: #5b8cf8;
transform: scaleX(0);
transform-origin: left center;
transition: transform 300ms ease-out;
}
.rs--hit .rs__fill {
background: #ff7a5e;
}
.rs__row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font: 600 13px/1.2 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #ffffff;
}
.rs__score b {
font-variant-numeric: tabular-nums;
}
.rs__warn {
color: #ff7a5e;
opacity: 0;
transform: scale(0.7);
transform-origin: right center;
transition: opacity 300ms ease-out, transform 300ms ease-out;
}
@media (max-width: 340px) {
.rs {
max-width: 260px;
}
.rs__input {
padding: 8px 12px;
font-size: 14px;
}
.rs__row {
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.rs__fill, .rs__warn, .rs__chip {
transition: none;
}
}(function () {
var root = document.getElementById('rs');
var input = document.getElementById('rsInput');
var chipsBox = document.getElementById('rsChips');
var fill = document.getElementById('rsFill');
var scoreEl = document.getElementById('rsScore');
var warn = document.getElementById('rsWarn');
var penaltyEl = document.getElementById('rsPenalty');
var PER_CHAR_PENALTY = 8;
// 계산 겹: 같은 글자 3연속(반복) 또는 문자코드가 1씩 오르내리는 3연속(연속)을
// 찾아 그 위치들을 표시하고, 표시된 Length × 감점값을 기본 점수에서 뺀다.
function flagPositions(value) {
var n = value.length;
var flag = new Array(n).fill(false);
var i = 0;
while (i < n) {
var j = i;
while (j + 1 < n && value.charCodeAt(j + 1) === value.charCodeAt(j)) j++;
if (j - i + 1 >= 3) for (var k = i; k <= j; k++) flag[k] = true;
i = j + 1;
}
i = 0;
while (i < n - 1) {
var dir = value.charCodeAt(i + 1) - value.charCodeAt(i);
if (dir === 1 || dir === -1) {
var j2 = i + 1;
while (j2 + 1 < n && value.charCodeAt(j2 + 1) - value.charCodeAt(j2) === dir) j2++;
if (j2 - i + 1 >= 3) for (var k2 = i; k2 <= j2; k2++) flag[k2] = true;
i = j2;
} else {
i++;
}
}
return flag;
}
function scoreOf(value) {
var base = Math.min(100, value.length * 8);
var flag = flagPositions(value);
var hitCount = flag.filter(Boolean).length;
var penalty = hitCount * PER_CHAR_PENALTY;
var score = Math.max(0, base - penalty);
return { score: score, flag: flag, penalty: penalty };
}
function paint(value) {
var r = scoreOf(value);
fill.style.transform = 'scaleX(' + (r.score / 100) + ')';
root.classList.toggle('rs--hit', r.penalty > 0);
scoreEl.textContent = String(r.score);
penaltyEl.textContent = '-' + r.penalty;
warn.style.opacity = r.penalty > 0 ? '1' : '0';
warn.style.transform = r.penalty > 0 ? 'scale(1)' : 'scale(.7)';
chipsBox.innerHTML = '';
for (var i = 0; i < value.length; i++) {
var chip = document.createElement('span');
chip.className = 'rs__chip' + (r.flag[i] ? ' rs__chip--on' : '');
chipsBox.appendChild(chip);
}
}
input.addEventListener('input', function () {
root.classList.remove('is-demo');
stopAutoplay();
paint(input.value);
});
paint(input.value);
// 자동 루프: 시드 고정 문자열 배열 — Repeat/sequence 구간이 있는 값과 없는 값을 번갈아 입력한다.
var SEQ = ['', 'q7Kd', 'aaaKd9', '12345Kd', 'abcdKf3', 'q7mR2z', 'aaa123', ''];
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 "./RepeatSequencePenalty.module.scss";
type Box<T> = { current: T };
const PER_CHAR_PENALTY = 8;
export default function RepeatSequencePenalty() {
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 chipsBox = root.querySelector(`.${styles.chips}`) as HTMLElement | null;
const fill = root.querySelector(`.${styles.fill}`) as HTMLElement | null;
const scoreEl = root.querySelector(`.${styles.scoreNum}`) as HTMLElement | null;
const warn = root.querySelector(`.${styles.warn}`) as HTMLElement | null;
const penaltyEl = root.querySelector(`.${styles.penaltyNum}`) as HTMLElement | null;
if (!input || !chipsBox || !fill || !scoreEl || !warn || !penaltyEl) return;
// 계산 겹: 같은 글자 3연속(반복) 또는 문자코드 증감 1씩 3연속(연속)을 찾아
// 그 위치들을 표시하고, 표시된 글자 수 × 감점값을 기본 점수에서 뺀다.
function flagPositions(value: string) {
const n = value.length;
const flag = new Array(n).fill(false);
let i = 0;
while (i < n) {
let j = i;
while (j + 1 < n && value.charCodeAt(j + 1) === value.charCodeAt(j)) j++;
if (j - i + 1 >= 3) for (let k = i; k <= j; k++) flag[k] = true;
i = j + 1;
}
i = 0;
while (i < n - 1) {
const dir = value.charCodeAt(i + 1) - value.charCodeAt(i);
if (dir === 1 || dir === -1) {
let j2 = i + 1;
while (j2 + 1 < n && value.charCodeAt(j2 + 1) - value.charCodeAt(j2) === dir) j2++;
if (j2 - i + 1 >= 3) for (let k2 = i; k2 <= j2; k2++) flag[k2] = true;
i = j2;
} else {
i++;
}
}
return flag as boolean[];
}
function scoreOf(value: string) {
const base = Math.min(100, value.length * 8);
const flag = flagPositions(value);
const hitCount = flag.filter(Boolean).length;
const penalty = hitCount * PER_CHAR_PENALTY;
const score = Math.max(0, base - penalty);
return { score, flag, penalty };
}
function paint(value: string) {
const r = scoreOf(value);
fill!.style.transform = "scaleX(" + r.score / 100 + ")";
root!.classList.toggle(styles["rs--hit"], r.penalty > 0);
scoreEl!.textContent = String(r.score);
penaltyEl!.textContent = "-" + r.penalty;
warn!.style.opacity = r.penalty > 0 ? "1" : "0";
warn!.style.transform = r.penalty > 0 ? "scale(1)" : "scale(.7)";
chipsBox!.innerHTML = "";
for (let i = 0; i < value.length; i++) {
const chip = document.createElement("span");
chip.className = styles.chip + (r.flag[i] ? " " + styles["chip--on"] : "");
chipsBox!.appendChild(chip);
}
}
function onInput() {
root!.classList.remove(styles["is-demo"]);
stopAutoplay();
paint(input!.value);
}
input.addEventListener("input", onInput);
paint(input.value);
// 자동 루프: 시드 고정 문자열 배열 — 반복·연속 구간이 있는 값과 없는 값을 번갈아 입력한다.
const SEQ = ["", "q7Kd", "aaaKd9", "12345Kd", "abcdKf3", "q7mR2z", "aaa123", ""];
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.rs} ${styles["is-demo"]}`}>
<label className={styles.field}>
<input className={styles.input} type="text" autoComplete="off" spellCheck={false} defaultValue="" />
</label>
<div className={styles.chips} />
<div className={styles.track}><div className={styles.fill} /></div>
<div className={styles.row}>
<span className={styles.score}>값 <b className={styles.scoreNum}>0</b>/100</span>
<span className={styles.warn}>반복·연속 <b className={styles.penaltyNum}>-0</b></span>
</div>
</div>
);
}05 Repeat/sequence penalty — the strength value drops by an amount tied to a run of the same character or a sequential digit value — Measured area 3.086% · moving frames 22/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
Inputs04 Wheel delta — a stepper whose value changes by the amount and direction the wheel moves over itNumber Stepper UI: 9 Interaction Patterns
Inputs01 Digit-group value toggle — as the entered digit value reaches every fourth position, a spacing value automatically appears between groupsCredit 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