01Input with icon or unit prefix/suffix
An email icon sits on the left and a unit label on the right, so you know what to type before you start.
Best forSignup email field, unit fields like weight or height
아이콘 접두단위 접미포커스 테두리
<main class="stage">
<div class="affix is-demo" id="wrap">
<div class="affix__field">
<label class="affix__label" for="email-input">Email</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">Weight</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 Input with icon or unit prefix/suffix — Measured area 1.326% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · Register Form Design: 9 Keyboard-Friendly Inputs
The post this demo lives in
Register Form Design: 9 Keyboard-Friendly InputsRegister form design lives or dies in the fields, not the submit button. These nine inputs cover everything a signup scr See all demos in this post
If you like this effect - similar demos
Inputs03 Currency input with auto commaCSS Input Design: 9 Form Widgets You Can Paste In
Inputs08 Pattern-match value toggle — when the leading digit value matches a set pattern value, a shape value beside it smoothly changesCredit Card Input UI: 9 Patterns
Inputs03 An Input Whose Underline Chases the Caret with InertiaCSS Floating Label: 9 Inputs on Spring Physics