회원가입 폼 디자인 입력창 9가지 — 붙이면 바로 되는 코드
회원가입 폼 디자인에서 사람이 처음으로 손대는 곳은 버튼이 아니라 입력창입니다. 여기 아홉 개는 이메일 칸부터 지우기 버튼까지 회원가입 화면에 실제로 들어가는 입력창이고, 코드를 붙이면 그대로 움직입니다.
자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip
- 01 아이콘·단위 붙는 입력창
- 02 뜨는 라벨 입력창
- 03 날짜 자동 슬래시 입력창
- 04 국가 코드 선택 전화번호 입력창
- 05 주소 자동완성 스타일 입력창
- 06 다중 선택 콤보박스
- 07 멘션·해시태그 강조 입력창
- 08 글자수 카운터 입력창
- 09 지우기(X) 버튼 입력창
늘어놓은 순서는 화면에 나오는 순서입니다. 맨 위의 이메일 칸(01)은 아이콘과 단위로 무엇을 적는 칸인지 먼저 알려 주고, 이름 칸(02)은 라벨이 위로 떠서 자리를 아끼고, 생년월일(03)과 전화번호(04)는 숫자를 사람이 읽는 모양으로 바꿔 줍니다. 그다음은 주소(05)와 관심 분야(06)처럼 목록에서 골라 채우는 칸이고, 마지막 셋은 소개글을 쓰는 동안 일어나는 일들입니다. 멘션과 해시태그(07)는 쓰는 중에 색이 들어오고, 글자수(08)는 한도를 지켜 주고, 지우기 버튼(09)은 다시 쓰고 싶을 때 한 번에 비워 줍니다. 목록이 열리는 셋(04·05·06)은 role="listbox"와 aria-activedescendant로 화살표 키가 실제로 듣고, Escape는 04·05·06·09 넷이 받습니다.
01아이콘·단위 붙는 입력창
왼쪽의 이메일 아이콘과 오른쪽의 kg 글자가 절대 위치로 붙어 있어, 칸에 손대기 전에 무엇을 적는지 알려 줍니다. 포커스가 오면 2px 테두리와 아이콘·단위 색이 같은 색으로 바뀌고, 아이콘과 단위는 aria-hidden="true"에 pointer-events: none이라 클릭을 가로채지 않습니다.
&__input {
box-sizing: border-box;
width: 100%;
border: 2px solid transparent;
border-radius: $r-control;
padding: $sp-3 $sp-4;
font-weight: 500;
font-size: clamp(14px, 4.2vw, 16px);
font-family: inherit;
color: $stage-ink;
background: #fff;
transition: border-color $duration $easing;
&--prefix { padding-left: $sp-10; }
&--suffix { padding-right: $sp-8; }
&:focus {
outline: none;
border-color: $color;
& ~ .affix__unit { color: $color; }
}
}
02뜨는 라벨 입력창
빈 칸에서는 라벨이 칸 안 가운데 놓여 있다가, 포커스를 받거나 글자가 하나라도 들어차면 translateY(-14px) scale(.78)로 위로 떠서 작아집니다. JS 없이 :focus와 :not(:placeholder-shown) 두 가상 클래스만으로 하며, 칸에 넣어 둔 placeholder=" " 한 칸이 이 트릭의 재료입니다.
&__label {
position: absolute;
left: $sp-4;
top: 50%;
transform: translateY(-50%) scale(1);
transform-origin: left center;
font-size: 16px;
font-weight: 500;
font-family: inherit;
color: rgba(23, 20, 26, .48);
pointer-events: none;
transition: transform $duration $easing, color $duration $easing;
}
&__input:focus + &__label,
&__input:not(:placeholder-shown) + &__label {
transform: translateY(-14px) scale(.78);
color: $color;
}
03날짜 자동 슬래시 입력창
숫자만 치면 2026/09/11처럼 넉 자리와 두 자리 뒤에 슬래시가 붙습니다. 달력을 여는 게 아니라 inputmode="numeric"을 단 글칸이라 모바일에서 숫자 자판이 먼저 뜨고, 8자리를 넘는 숫자는 잘립니다.
function format(raw) {
var digits = raw.replace(/\D/g, '').slice(0, 8);
var out = digits.slice(0, 4);
if (digits.length > 4) out += '/' + digits.slice(4, 6);
if (digits.length > 6) out += '/' + digits.slice(6, 8);
return out;
}
input.addEventListener('input', function () {
input.value = format(input.value);
});
04국가 코드 선택 전화번호 입력창
국기 버튼으로 나라를 고르면 국가번호가 바뀌고, 번호 칸은 그 나라의 자릿수 묶음대로 다시 쓰입니다. 대한민국 +82는 3·4·4, 미국 +1은 3·3·4, 일본 +81은 2·4·4로 끊겨서 같은 열 자리라도 읽는 모양이 다릅니다.
trigger.addEventListener('keydown', function (e) {
if (!open) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
openListbox();
}
return;
}
if (e.key === 'ArrowDown') { e.preventDefault(); highlight((active + 1) % options.length); }
else if (e.key === 'ArrowUp') { e.preventDefault(); highlight((active - 1 + options.length) % options.length); }
else if (e.key === 'Enter') { e.preventDefault(); selectCountry(options[active].dataset.code); closeListbox(true); }
else if (e.key === 'Escape') { e.preventDefault(); closeListbox(true); }
else if (e.key === 'Tab') { closeListbox(false); }
});
05주소 자동완성 스타일 입력창
주소를 치면 아래에 후보 다섯 줄 중 맞는 것만 남고, 화살표와 Enter로 골라 칸을 채웁니다. 입력창은 role="combobox"에 aria-autocomplete="list"를 달았고, 후보 줄은 미리 정해 둔 주소에서 골라 만드는 정적 목록이라 API 키가 필요 없습니다.
input.addEventListener('keydown', function (e) {
if (e.key === 'ArrowDown') {
e.preventDefault();
if (!open && matches.length) { openList(); setActive(0); return; }
if (open && matches.length) setActive(Math.min(active + 1, matches.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (open) { if (active <= 0) setActive(-1); else setActive(active - 1); }
} else if (e.key === 'Enter') {
if (open && active > -1) { e.preventDefault(); selectOption(active, true); }
} else if (e.key === 'Escape') {
if (open) { e.preventDefault(); closeList(); setActive(-1); }
}
});
06다중 선택 콤보박스
디자인·개발·마케팅·기획 네 줄에서 여러 개를 고르면 고른 항목마다 알약 칩이 입력칸 안에 쌓입니다. 목록에 aria-multiselectable="true"가 붙어 있어 둘째를 골라도 첫째가 풀리지 않고, 칩은 chip-pop 키프레임으로 살짝 지나쳤다가 제 크기에 안착합니다.
@keyframes chip-pop {
0% { transform: scale(.5); opacity: 0; }
70% { transform: scale(1.06); opacity: 1; }
100% { transform: scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
.multi__listbox { transition: none !important; }
.multi__trigger, .multi__chevron, .multi__check { transition: none; }
.multi__chip { animation: none; }
}
07멘션·해시태그 강조 입력창
@이름과 #태그를 치는 순간 그 단어에만 색이 칠해집니다. contenteditable을 쓰지 않고, 글자를 숨긴 textarea와 그 아래 깔린 backdrop 두 겹을 같은 폭·같은 줄바꿈 규칙으로 겹쳐서 커서가 제자리에 살아 있게 했습니다.
&__backdrop,
&__input {
box-sizing: border-box;
width: 100%;
min-height: 96px;
max-height: 160px;
padding: $sp-3 $sp-4;
border: 2px solid transparent;
border-radius: $r-control;
font-weight: 500;
font-size: clamp(13px, 3.8vw, 15px);
line-height: 1.5;
font-family: inherit;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
08글자수 카운터 입력창
오른쪽 아래 숫자가 0/80부터 올라가고, 남은 글자가 12자 이하가 되면 숫자와 테두리가 빨갛게 바뀝니다. 한도는 HTML maxlength="80"이 지키고, 숫자는 aria-live="polite"로 읽어 주기 때문에 화면을 보지 않고도 한도를 알 수 있습니다.
function update() {
var len = area.value.length;
var remaining = MAX - len;
count.textContent = len + '/' + MAX;
if (remaining <= WARN) {
count.classList.add('is-warn');
area.classList.add('is-warn');
} else {
count.classList.remove('is-warn');
area.classList.remove('is-warn');
}
}
area.addEventListener('input', update);
09지우기(X) 버튼 입력창
한 글자라도 치면 오른쪽에 동그란 X 버튼이 나타나고, 누르면 칸이 비워지면서 초점이 그 칸에 그대로 남습니다. 버튼은 값이 없으면 opacity: 0에 pointer-events: none으로 완전히 물러나 있고, 값이 생기는 순간만 살아납니다.
&__clear {
position: absolute;
top: 50%;
right: $sp-2;
transform: translateY(-50%) scale(.5);
width: 24px;
height: 24px;
display: grid;
place-items: center;
border: 0;
border-radius: 50%;
background: rgba(23, 20, 26, .08);
color: $stage-ink;
cursor: pointer;
opacity: 0;
pointer-events: none;
transition: opacity $duration $easing, transform $duration $easing, background $duration $easing;
&:hover, &:focus-visible { background: rgba(23, 20, 26, .16); }
}
어디서 깨지는가 — 함정
세로로 깨진 것은 320px 폭에서 나타났습니다. 07과 08은 둘 다 세 줄짜리 textarea를 품고 있는데, 입력칸의 최소 높이를 96px로 두자 카드 전체가 그 시점의 무대보다 07은 2px, 08은 3px 길어져 아래 테두리가 잘렸습니다. 데모 크기 480px에서는 한참 남던 여백이 좁은 화면에서는 없는 것과 같아서, 넓은 화면에서 눈으로 확인한 것만으로는 이 결함을 볼 수 없었습니다. 고친 방법은 입력창을 흐름에서 빼는 것이 아니라, 화면 폭이 340px 이하로 내려가는 순간부터 min-height를 96px에서 88px로 낮추는 @media (max-width: 340px) 블록을 추가하는 것이었습니다. 라벨과 힌트 글자는 그대로 두고 입력칸의 세로만 8px 줄였더니 두 항목 모두 넘침 판정이 사라졌습니다. 이 규칙을 넣은 소스는 압축 비밀번호 ttw779pg를 입력하면 열리는 zip 안에 아홉 벌 그대로 들어 있습니다.
접근성
아홉 개 전부 prefers-reduced-motion: reduce에서 전환을 끄고 상태 값은 남깁니다. 테두리와 아이콘 색은 전환 없이 즉시 바뀌고, 떠오르던 라벨은 떠오른 자리에 그대로 서며, 06의 칩은 animation: none으로 튀지 않고 바로 나타납니다. 목록이 열리는 04·05·06은 aria-expanded로 열림을 알리고 aria-activedescendant로 초점을 옮기지 않은 채 가리키는 줄만 바꾸며, 08의 글자수는 aria-live="polite"로 읽힙니다. 초점을 되돌릴 때 focus({ preventScroll: true })를 쓰는 것은 이 데모들이 글 안에 iframe으로 들어 있어, 그냥 focus()를 부르면 읽던 사람의 화면이 아래로 끌려가기 때문입니다. 여기 쓴 입력 속성의 뜻은 MDN의 input 요소 문서에 정리돼 있습니다.
같은 자리에 쓸 만한 부품은 폼·입력 카테고리에, 입력이 틀렸을 때 흔들리는 반응은 에러 카테고리에 모아 두었습니다.
FAQ
달력이 열리는 생년월일 칸을 두고 왜 직접 만드나요?
브라우저가 주는 달력은 호환성이 좋지만 모양이 브라우저마다 다르고, 키보드로 빠르게 치는 사람에게는 열고 닫는 동안이 오히려 느립니다. 이 칸은 숫자만 받아 2026/09/11 모양으로 붙여 주는 글칸이라 달력 없이도 서식이 흐트러지지 않습니다. 달력이 필요한 화면이라면 이 칸과 달력을 함께 두는 편입니다.
아홉 개 전부 키보드만으로 쓸 수 있나요?
네. 01·02·03·07·08은 평범한 input과 textarea라 브라우저가 기본으로 하는 일을 그대로 씁니다. 목록이 열리는 04·05·06은 화살표로 옮기고 Enter로 고르고 Escape로 닫으며, 04와 06은 양 끝에서 화살표가 감아서 돌아가고 Tab으로 나가면 목록이 닫힙니다. 09는 입력 칸에서 Escape를 누르면 값이 지워집니다.
React 버전에서 상한이나 색을 바꿀 수 있나요?
08의 글자수 상한은 max prop으로 바꿀 수 있고 기본값은 80입니다. 경고가 시작되는 남은 글자수 12는 컴포넌트 파일 안의 상수라 값을 바꾸려면 그 줄을 고칩니다. 색과 속도는 React의 module.scss에 직접 적힌 값을 고치면 되고, 데모의 style.scss는 맨 위의 $duration·$easing·$color 세 줄만 바꾸면 됩니다.