Forms That Feel Good
Forms are where UX lives. A well-built form has good defaults, clear error states, accessible labels, and micro-interactions that feel polished. Claude Code can handle all of it.
Starting prompt
로그인 폼을 만들어줘.
- 이메일 + 비밀번호 필드
- "로그인" 버튼 (primary, full-width)
- "비밀번호 찾기" 텍스트 링크
- 구분선 + "Google로 계속하기" 소셜 버튼
- 흰 카드, max-width 400px, center on page
- Pretendard 폰트 사용
Adding validation
Once you have the structure:
이메일 필드에 blur 이벤트로 유효성 검사 추가해줘.
- 형식이 맞지 않으면 필드 아래에 에러 메시지 표시
- 에러 시 border-color: #ef4444
- 성공 시 border-color: #22c55e
- 메시지는 role="alert" aria 속성 포함
Password toggle
비밀번호 필드 오른쪽 안쪽에 표시/숨김 토글 버튼 추가해줘.
- 눈 아이콘 SVG (inline, 16px)
- 클릭하면 type="password" ↔ type="text" 전환
- 버튼은 배경 없음, border 없음
Loading state
로그인 버튼 클릭 시 로딩 상태 추가해줘.
- 텍스트 → 스피너 (CSS border-radius spin animation)
- 클릭 중 버튼 disabled
- 2초 후 원래 상태로 (시뮬레이션용)
What the demo shows
The demo is the finished login form — with validation, password toggle, social button, and loading state — exactly what you’d build through the prompts above. Read the source code to see how all the pieces fit together.
폼은 UX가 살아있는 곳입니다. 잘 만든 폼은 좋은 기본값, 명확한 에러 상태, 접근 가능한 레이블, 완성도 높은 마이크로 인터랙션을 갖습니다. Claude Code가 이 모든 것을 처리할 수 있습니다.
시작 프롬프트
로그인 폼을 만들어줘.
- 이메일 + 비밀번호 필드
- "로그인" 버튼 (primary, full-width)
- "비밀번호 찾기" 텍스트 링크
- 구분선 + "Google로 계속하기" 소셜 버튼
- 흰 카드, max-width 400px, center on page
유효성 검사 추가
구조가 완성되면:
이메일 필드에 blur 이벤트로 유효성 검사 추가해줘.
- 형식이 맞지 않으면 필드 아래에 에러 메시지 표시
- 에러 시 border-color: #ef4444
- 성공 시 border-color: #22c55e
유효성 검사는 항상 UX 관점에서 설명하세요: 언제 발동되는지, 어디에 표시되는지, 어떻게 보이는지.
접근성 향상
모든 폼 필드에 적절한 label을 연결하고,
에러 메시지를 aria-describedby로 연결해줘.
서브밋 시 loading 상태에서 버튼을 비활성화해줘.
데모는 유효성 검사, 에러 상태, 로딩 피드백, 접근성을 갖춘 완성된 로그인 폼을 보여줍니다.
Source Code script.js
// ── Password toggle ───────────────────────────────────────
const pwInput = document.getElementById('password');
const pwToggle = document.getElementById('pwToggle');
const eyeIcon = document.getElementById('eyeIcon');
pwToggle.addEventListener('click', () => {
const show = pwInput.type === 'password';
pwInput.type = show ? 'text' : 'password';
pwToggle.setAttribute('aria-label', show ? 'Hide password' : 'Show password');
eyeIcon.innerHTML = show
? `<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/>
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/>
<line x1="1" y1="1" x2="23" y2="23"/>`
: `<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`;
});
// ── Email validation ──────────────────────────────────────
const emailInput = document.getElementById('email');
const emailField = document.getElementById('emailField');
const emailMsg = document.getElementById('emailMsg');
function validateEmail() {
const v = emailInput.value.trim();
if (!v) {
setFieldState(emailField, emailMsg, '', '');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) {
setFieldState(emailField, emailMsg, 'error', 'Please enter a valid email address');
} else {
setFieldState(emailField, emailMsg, 'success', '');
}
}
emailInput.addEventListener('blur', validateEmail);
emailInput.addEventListener('input', () => {
if (emailField.classList.contains('error')) validateEmail();
});
// ── Password validation ───────────────────────────────────
const passwordField = document.getElementById('passwordField');
const passwordMsg = document.getElementById('passwordMsg');
function validatePassword() {
const v = pwInput.value;
if (!v) {
setFieldState(passwordField, passwordMsg, '', '');
} else if (v.length < 6) {
setFieldState(passwordField, passwordMsg, 'error', 'Password must be at least 6 characters');
} else {
setFieldState(passwordField, passwordMsg, 'success', '');
}
}
pwInput.addEventListener('blur', validatePassword);
pwInput.addEventListener('input', () => {
if (passwordField.classList.contains('error')) validatePassword();
});
function setFieldState(field, msgEl, state, msg) {
field.classList.remove('error', 'success');
if (state) field.classList.add(state);
msgEl.textContent = msg;
}
// ── Form submit with loading state ───────────────────────
const form = document.getElementById('loginForm');
const submitBtn = document.getElementById('submitBtn');
form.addEventListener('submit', e => {
e.preventDefault();
validateEmail();
validatePassword();
const hasError = emailField.classList.contains('error') || passwordField.classList.contains('error');
const isEmpty = !emailInput.value.trim() || !pwInput.value;
if (hasError || isEmpty) {
if (!emailInput.value.trim()) setFieldState(emailField, emailMsg, 'error', 'Email is required');
if (!pwInput.value) setFieldState(passwordField, passwordMsg, 'error', 'Password is required');
return;
}
// Simulate API call
submitBtn.disabled = true;
submitBtn.classList.add('loading');
setTimeout(() => {
submitBtn.disabled = false;
submitBtn.classList.remove('loading');
// In a real app: redirect on success
setFieldState(emailField, emailMsg, 'error', 'Invalid email or password');
setFieldState(passwordField, passwordMsg, 'error', ' ');
}, 2000);
});