CSS 강좌 / 폼 상태와 키보드 초점 꾸미기
폼의 모양은 CSS로 바꾸고, 필수 여부와 기본 형식 검사는 HTML의 required·type 같은 속성으로 정합니다. 키보드 초점, 잘못된 입력, 비활성 상태는 서로 다른 의미이므로 색과 함께 이름·설명·오류 문장을 제공해야 합니다. CSS로 빨간 테두리를 만들었다고 입력 검증이나 오류 설명이 완성되는 것은 아닙니다.
항목 이름과 설명부터 연결하기
label의 for와 입력창의 id를 같게 지정하면 항목 이름과 입력창이 연결됩니다. placeholder는 입력을 시작하면 사라지므로 이름이나 중요한 조건을 대신하지 않습니다. 추가 설명은 계속 보이는 문장으로 두고 aria-describedby에 해당 문장의 id를 지정합니다.
<label for="email">이메일 주소 (필수)</label> <input id="email" name="email" type="email" required placeholder="name@example.com" aria-describedby="email-help"> <p id="email-help">name@example.com 같은 형식으로 입력하세요.</p>
위 부분 코드는 아래 전체 예제의 이메일 구역을 간략하게 나타냅니다. id는 문서에서 대상을 구별하고, name은 제출 데이터의 항목 이름이 됩니다. name이 없으면 화면에서 입력할 수 있어도 일반적인 제출 항목에 포함되지 않습니다. 레이블과 설명의 구성은 W3C 폼 레이블 안내와 폼 입력 안내를 참고하세요.
기본 입력창이 작은 화면과 큰 글자에 맞도록 만들기
입력창의 고유 너비에만 맡기면 글자 크기를 늘렸을 때 좁은 화면을 넘어갈 수 있습니다. 양식의 최대 너비를 정하고 입력창에는 width: 100%와 box-sizing: border-box를 함께 사용합니다. 그러면 테두리와 안쪽 여백도 지정한 너비 안에 포함됩니다.
*, *::before, *::after { box-sizing: border-box; }
form { max-width: 28rem; }
input { width: 100%; min-width: 0; padding: .65rem; font: inherit; }
이 규칙은 전체 예제의 style 영역에 들어 있습니다. font: inherit는 입력창과 버튼이 본문의 글꼴·크기·줄 높이를 따르게 하며, min-width는 Flexbox·Grid 안에 입력창을 넣을 때도 축소를 방해하는 최소 크기를 줄이는 데 도움이 됩니다. 설명을 늘리거나 글자를 키워도 아래 항목이 자연스럽게 내려가도록 고정 높이는 사용하지 않습니다.
초점과 입력 오류를 구분하기
:focus는 현재 초점을 가진 요소에 일치하고, :focus-visible은 브라우저가 초점 표시가 필요하다고 판단한 경우에 일치합니다. 키보드 탐색 때 주로 표시되지만 텍스트 입력창에서는 마우스로 눌러도 표시될 수 있으므로 “키보드에서만 적용된다”고 이해하면 안 됩니다. MDN focus-visible 설명에 판단 방식이 나와 있습니다. 초점 표시를 링크에도 일관되게 적용하려면 링크의 키보드 포커스 표시와 비교하세요.
초점은 지금 조작하는 항목을, 오류는 입력 조건을 충족하지 못한 항목을 나타냅니다. 두 상태가 동시에 있을 수 있으므로 전체 예제는 초점을 파란 윤곽선, 오류를 진한 빨간 테두리와 오류 문장으로 표시합니다. 외곽 윤곽선이 잘리지 않도록 주변에 여유를 두고, outline을 없앤 뒤 다른 초점 표시를 제공하지 않는 방식은 피합니다.
:invalid는 현재 값이 유효하지 않으면 일치하므로 빈 required 입력창도 처음부터 해당합니다. :user-invalid는 사용자 상호작용 뒤 브라우저가 오류 상태를 표시할 시점에 일치합니다. 입력을 끝내거나 제출을 시도하는 등 표시 시점은 브라우저의 동작에 따라 달라질 수 있습니다. 실습은 제출 버튼을 눌러 오류를 확인합니다. MDN user-invalid 설명을 참고하세요.
오래된 지원 대상까지 고려해야 한다면 선택자의 지원 여부를 확인하고 별도 검증 흐름을 설계해야 합니다. 선택자가 지원되지 않아 오류 꾸미기가 나타나지 않더라도 HTML의 필수·형식 검사까지 사라지는 것은 아닙니다. 초점 색은 실제 배경과 충분히 구분되게 고르고, W3C 비텍스트 대비 안내의 적용 조건도 확인하세요.
필수·유효성·비활성·읽기 전용의 차이
| 상태 | 의미 | 초점과 일반적인 제출 |
|---|---|---|
required |
사용자가 값을 입력해야 함 | 활성 항목이면 초점 가능; 빈 값은 제출 시 검증 대상 |
:valid·:invalid |
HTML 검증 조건 충족 여부 | 테두리 모양과 별개로 검증 결과가 정해짐 |
disabled |
현재 사용 불가 | 일반적으로 Tab 탐색과 제출 항목에서 제외 |
readonly |
지원하는 텍스트 입력 등의 값을 수정할 수 없음 | 초점과 선택 가능; name이 있으면 제출 항목에 포함 |
읽기 전용은 모든 폼 요소에서 쓸 수 있는 공통 상태가 아닙니다. 이 예제처럼 지원하는 텍스트 입력에 적용해야 하며, select를 readonly로 만드는 용도로 사용하지 않습니다. 또한 readonly 항목은 일반적인 제약 검증 대상에서 제외됩니다. 실제 제출 규칙은 HTML 표준의 데이터 목록 구성과 MDN readonly 안내를 참고하세요.
이메일 예시에는 name@example.com을 사용합니다. 이 형식이 검사에 통과한다고 계정이 실제로 존재하거나 메일이 도착한다는 뜻은 아닙니다. type="email"의 문법 검사는 국제화된 모든 주소 형식을 포괄하는 검사가 아니며, 한글을 @ 앞에 넣은 예시를 이 입력창의 통과 예시로 제공하지 않습니다. HTML 표준의 이메일 입력 조건이 적용 범위를 정합니다.
전체 예제: 상태 표시와 제출 항목 확인
다음 코드를 UTF-8의 form-states.html로 저장하고 브라우저에서 여세요. HTML과 CSS가 폼 상태를 결정합니다. 끝의 짧은 JavaScript는 유효한 제출을 가로채 데이터 목록을 화면에 보여 주기 위한 보조 코드이며, 서버로 전송하거나 별도 입력 검사를 수행하지 않습니다. JavaScript가 꺼져 있으면 결과 출력 기능은 동작하지 않습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>폼 상태와 제출 항목 비교</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; padding: 1rem; color: #183044; font-family: sans-serif; line-height: 1.6; }
main { max-width: 46rem; margin-inline: auto; }
h1 { font-size: 1.5rem; } h2 { font-size: 1.2rem; }
button, input { font: inherit; }
button { cursor: pointer; }
:focus-visible { outline: 3px solid #164f73; outline-offset: 3px; }
form { max-width: 28rem; }
.field { margin-block: 1.25rem; }
label { display: block; margin-bottom: .35rem; font-weight: 700; }
input { display: block; width: 100%; min-width: 0; padding: .65rem;
border: 2px solid #6d7e8b; border-radius: .3rem; background: white; color: #183044; }
input:required { border-inline-start-width: 4px; }
input:user-invalid { border-color: #9d2632; }
.help, .error { margin-block: .4rem 0; }
.error { display: none; color: #9d2632; }
input:user-invalid + .error { display: block; }
input[readonly] { background: #eef6fa; }
input:disabled { background: #eceff2; color: #465766; cursor: not-allowed; }
button { padding: .65rem 1rem; border: 1px solid #164f73; border-radius: .3rem;
background: #166b92; color: white; }
output { display: block; margin-block: 1rem; white-space: pre-wrap; overflow-wrap: anywhere; }
</style>
</head>
<body>
<main>
<h1>입력 상태와 제출 데이터 확인</h1>
<form id="demo-form">
<div class="field">
<label for="email">이메일 주소 (필수)</label>
<input id="email" name="email" type="email" autocomplete="email" required
placeholder="name@example.com" aria-describedby="email-help email-error">
<p id="email-error" class="error">이메일을 비워 두지 말고 name@example.com 같은 형식으로 입력하세요.</p>
<p id="email-help" class="help">형식을 확인하는 실습용 입력창입니다. 예시 주소의 실제 수신 여부는 확인하지 않습니다.</p>
</div>
<div class="field">
<label for="plan">요금제 (읽기 전용)</label>
<input id="plan" name="plan" type="text" value="기본" readonly>
</div>
<div class="field">
<label for="coupon">쿠폰 (현재 비활성)</label>
<input id="coupon" name="coupon" type="text" value="WELCOME" disabled>
</div>
<button type="submit">제출 항목 확인</button>
</form>
<output id="result" for="email plan coupon" aria-live="polite"></output>
<script>
const form = document.getElementById("demo-form");
form.addEventListener("submit", (event) => {
event.preventDefault();
const entries = Array.from(new FormData(form).entries());
document.getElementById("result").textContent = JSON.stringify(entries, null, 2);
});
</script>
</main>
</body>
</html>
- 이메일을 비운 채 제출 항목 확인을 누릅니다. 브라우저의 필수 입력 안내와 오류 표시를 확인합니다.
- 이메일에
abc를 입력하고 다시 제출합니다. 형식 오류로 결과 목록이 새로 출력되지 않아야 합니다. name@example.com을 입력하고 제출합니다. 결과에는 email과 plan이 있고 disabled인 coupon은 없습니다.- Tab 키로 이메일→요금제→제출 버튼을 탐색합니다. 비활성 쿠폰은 일반적인 Tab 순서에서 건너뛰고, 읽기 전용 요금제는 초점을 받을 수 있습니다.
- 전체 예제에서 쿠폰의 disabled 속성만 제거합니다. 쿠폰에 초점을 둘 수 있고 제출 목록에 값이 추가됩니다. 확인 후 disabled를 복원합니다.
- 요금제의 readonly 속성만 제거하면 값을 바꿀 수 있습니다. 실습 후 값을 “기본”으로 돌리고 readonly를 복원합니다.
- 화면 폭을 줄이고 브라우저 확대나 글자 크기 설정을 조절합니다. 입력창·초점 윤곽선·오류 문장이 잘리거나 겹치지 않는지 확인합니다.
CSS와 브라우저 검증의 역할을 넘겨짚지 않기
CSS는 항목의 상태를 보여 주는 역할입니다. 브라우저의 검증은 사용자의 입력 실수를 줄일 수 있지만 개발자 도구나 별도 요청으로 우회할 수 있으므로, 실제 서비스는 서버에서 허용 값·길이·권한 등을 다시 검사해야 합니다. 오류 문장은 문제가 있는 항목과 해결 방법을 구체적으로 설명하고, 서버 오류가 생기면 입력 내용을 보존하는 흐름도 별도로 설계합니다.
실습이 예상과 다르면 label의 for와 id, 제출용 name, required와 input type, disabled·readonly 여부를 먼저 확인하세요. 초점 표시가 없으면 다른 outline 규칙이 덮어쓰는지 살펴봅니다. 오류가 처음부터 나타나면 :invalid를 사용했는지, 제출 뒤에도 문장이 안 나타나면 :user-invalid 지원과 형제 선택자의 HTML 순서를 확인하면 원인을 좁힐 수 있습니다.










