01아이콘·단위 붙는 입력창
이메일 아이콘이 왼쪽에, 단위 글자가 오른쪽에 붙어 이 칸에 뭘 넣는지 입력 전에 알려줍니다.
쓰임회원가입 이메일 입력, 몸무게·키 같은 단위 입력
아이콘 접두단위 접미포커스 테두리
<main class="stage">
<div class="affix is-demo" id="wrap">
<div class="affix__field">
<label class="affix__label" for="email-input">이메일</label>
<div class="affix__control">
<svg class="affix__icon" aria-hidden="true" viewBox="0 0 20 20" width="16" height="16" fill="none">
<rect x="2" y="4" width="16" height="12" rx="2" stroke="currentColor" stroke-width="1.6"/>
<path d="M2.5 5L10 11L17.5 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<input class="affix__input affix__input--prefix" id="email-input" type="text" placeholder="[email protected]">
</div>
</div>
<div class="affix__field">
<label class="affix__label" for="weight-input">몸무게</label>
<div class="affix__control">
<input class="affix__input affix__input--suffix" id="weight-input" type="text" placeholder="0">
<span class="affix__unit" aria-hidden="true">kg</span>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-quick; // 테두리 색 전환 시간
$easing: $ease-out; // 포커스 이징
$color: $subject-blue; // 포커스 강조 색
// ── 데모 무대 ──────────────────────────────────────────
@include stage($stage-ink);
.affix {
box-sizing: border-box;
width: min(280px, 86vw);
background: $subject-cream;
border-radius: $r-card;
padding: $sp-4;
box-shadow: $shadow-raised;
display: flex;
flex-direction: column;
gap: $sp-4;
&__field {
display: flex;
flex-direction: column;
gap: $sp-1;
}
&__label {
display: block;
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
color: $stage-ink;
}
&__control {
position: relative;
display: flex;
align-items: center;
}
&__icon {
position: absolute;
left: $sp-3;
color: rgba(23, 20, 26, .45);
pointer-events: none;
transition: color $duration $easing;
}
&__unit {
position: absolute;
right: $sp-3;
font-size: 13px;
font-weight: 600;
font-family: inherit;
color: rgba(23, 20, 26, .45);
pointer-events: none;
transition: color $duration $easing;
}
&__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; }
}
}
&__control:focus-within &__icon { color: $color; }
}
@media (prefers-reduced-motion: reduce) {
.affix__input,
.affix__icon,
.affix__unit {
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);
}
.affix {
box-sizing: border-box;
width: min(280px, 86vw);
background: #fff7e6;
border-radius: 16px;
padding: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
display: flex;
flex-direction: column;
gap: 16px;
}
.affix__field {
display: flex;
flex-direction: column;
gap: 4px;
}
.affix__label {
display: block;
font-weight: 700;
font-size: 12px;
line-height: 1;
font-family: inherit;
color: #17141a;
}
.affix__control {
position: relative;
display: flex;
align-items: center;
}
.affix__icon {
position: absolute;
left: 12px;
color: rgba(23, 20, 26, 0.45);
pointer-events: none;
transition: color 220ms ease-out;
}
.affix__unit {
position: absolute;
right: 12px;
font-size: 13px;
font-weight: 600;
font-family: inherit;
color: rgba(23, 20, 26, 0.45);
pointer-events: none;
transition: color 220ms ease-out;
}
.affix__input {
box-sizing: border-box;
width: 100%;
border: 2px solid transparent;
border-radius: 12px;
padding: 12px 16px;
font-weight: 500;
font-size: clamp(14px, 4.2vw, 16px);
font-family: inherit;
color: #17141a;
background: #fff;
transition: border-color 220ms ease-out;
}
.affix__input--prefix {
padding-left: 40px;
}
.affix__input--suffix {
padding-right: 32px;
}
.affix__input:focus {
outline: none;
border-color: #2f6df6;
}
.affix__input:focus ~ .affix__unit {
color: #2f6df6;
}
.affix__control:focus-within .affix__icon {
color: #2f6df6;
}
@media (prefers-reduced-motion: reduce) {
.affix__input,
.affix__icon,
.affix__unit {
transition: none;
}
}(function () {
var wrap = document.getElementById('wrap');
var emailInput = document.getElementById('email-input');
var weightInput = document.getElementById('weight-input');
function stopDemo() {
if (!wrap.classList.contains('is-demo')) return;
wrap.classList.remove('is-demo');
clearInterval(timer);
}
emailInput.addEventListener('pointerdown', stopDemo);
emailInput.addEventListener('keydown', stopDemo);
weightInput.addEventListener('pointerdown', stopDemo);
weightInput.addEventListener('keydown', stopDemo);
function fireInput(el) {
el.dispatchEvent(new Event('input', { bubbles: true }));
}
// 각 단계: target(어느 input) · text(칠 문자열) · hold(다 친 뒤 멈춤 ms)
var script = [
{ el: emailInput, text: '[email protected]', hold: 900 },
{ el: weightInput, text: '58', hold: 900 }
];
var stepIdx = 0, chars = '', phase = 'focus', pauseUntil = 0;
var timer = setInterval(function () {
var now = Date.now();
if (now < pauseUntil) return;
var step = script[stepIdx];
if (phase === 'focus') {
step.el.focus({ preventScroll: true });
phase = 'type';
return;
}
if (phase === 'type') {
if (chars.length < step.text.length) {
chars += step.text[chars.length];
step.el.value = chars;
fireInput(step.el);
} else {
phase = 'hold';
pauseUntil = now + step.hold;
}
return;
}
if (phase === 'hold') {
step.el.value = '';
fireInput(step.el);
step.el.blur();
chars = '';
stepIdx = (stepIdx + 1) % script.length;
phase = 'focus';
pauseUntil = now + 220;
}
}, 140);
})();import { useState } from "react";
import styles from "./IconAffixInput.module.scss";
export type IconAffixInputProps = {
emailLabel?: string;
weightLabel?: string;
};
export default function IconAffixInput({
emailLabel = "이메일",
weightLabel = "몸무게",
}: IconAffixInputProps) {
const [email, setEmail] = useState("");
const [weight, setWeight] = useState("");
return (
<div className={styles.affix}>
<div className={styles.field}>
<label className={styles.label} htmlFor="email-input">
{emailLabel}
</label>
<div className={styles.control}>
<svg
className={styles.icon}
aria-hidden="true"
viewBox="0 0 20 20"
width="16"
height="16"
fill="none"
>
<rect x="2" y="4" width="16" height="12" rx="2" stroke="currentColor" strokeWidth="1.6" />
<path
d="M2.5 5L10 11L17.5 5"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
<input
className={`${styles.input} ${styles.inputPrefix}`}
id="email-input"
type="text"
placeholder="[email protected]"
value={email}
onChange={(e: any) => setEmail(e.target.value)}
/>
</div>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor="weight-input">
{weightLabel}
</label>
<div className={styles.control}>
<input
className={`${styles.input} ${styles.inputSuffix}`}
id="weight-input"
type="text"
placeholder="0"
value={weight}
onChange={(e: any) => setWeight(e.target.value)}
/>
<span className={styles.unit} aria-hidden="true">
kg
</span>
</div>
</div>
</div>
);
}01 아이콘·단위 붙는 입력창 — 측정 누적면적 1.326% · 움직인 프레임 11/23GODRICH 원작 · zip 에 SCSS·React 포함 · 회원가입 폼 디자인 입력창 9가지 — 붙이면 바로 되는 코드
이 데모가 속한 글
회원가입 폼 디자인 입력창 9가지 — 붙이면 바로 되는 코드회원가입 폼 디자인에서 사람이 처음으로 손대는 곳은 버튼이 아니라 입력창입니다. 여기 아홉 개는 이메일 칸부터 지우기 버튼까지 회원가입 화면에 실제로 들어가는 입력창이고, 코드를 붙이면 그대로 움직입니다. 이 글의 데모 전부 보기

