CSS / Tutorial / 표(table) 꾸미기 / 틀 고정(행 또는 열 고정) 만들기
표에서 행이나 열을 고정하려면 해당 셀에 position: sticky와 top: 0 또는 left: 0 같은 기준 위치를 지정합니다. 표를 높이·너비가 제한된 스크롤 영역으로 감싸면 그 영역 안에서 첫 행과 첫 열을 유지할 수 있습니다. 두 고정 영역이 만나는 모서리 셀에는 더 높은 z-index와 불투명 배경을 주어 다른 셀에 가려지지 않게 합니다.
틀 고정의 기본 원리와 스크롤 기준
sticky는 일반 흐름에서 공간을 차지하다가 스크롤 기준 위치에 닿으면 붙어 보이게 하는 위치 지정 방식입니다. display: sticky가 아니라 position: sticky입니다. 화면에 영구적으로 붙이는 position: fixed와도 다르며, 자신을 담는 영역의 범위 안에서 동작합니다.
.scroll-area {
max-height: 320px;
overflow: auto;
}
thead th {
position: sticky;
top: 0;
}
이 예제는 페이지 전체가 아닌 표를 감싼 영역을 스크롤합니다. 높이 제한이 없으면 내용에 따라 영역이 커져 내부 세로 스크롤이 생기지 않을 수 있습니다. 가로 스크롤을 보려면 표의 실제 너비도 영역보다 커야 합니다. top·left 같은 기준값을 생략하면 해당 축에서 붙는 위치가 정해지지 않습니다.
sticky는 스크롤 메커니즘을 만드는 가까운 조상 요소의 영향을 받습니다. 중간 조상에 overflow: hidden·auto 등이 있으면 기대한 페이지 스크롤 기준과 달라질 수 있으므로 실제 스크롤되는 영역을 확인하세요. 기준과 조건은 MDN position 문서에서 확인할 수 있습니다.
전체 예제: 첫 행·첫 열·마지막 행 함께 고정
아래 코드를 sticky-table.html로 저장하고 브라우저에서 엽니다. 외부 파일이나 JavaScript는 필요하지 않습니다. 수량은 동작 확인용 가상 데이터입니다. 표 영역의 최대 높이는 320px, 표의 최소 너비는 840px로 정해 가로와 세로 스크롤을 모두 만들었습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>표의 행과 열 틀 고정</title>
<style>
body { margin: 20px; font: 16px/1.6 "Noto Sans KR", sans-serif; color: #203348; }
* { box-sizing: border-box; }
h1 { font-size: 24px; }
.demo { --first-col: 120px; max-width: 680px; max-height: 320px;
overflow: auto; border: 1px solid #7795a3; isolation: isolate; }
.demo:focus-visible { outline: 3px solid #b75c00; outline-offset: 2px; }
.jb-table-01 { width: 100%; min-width: 840px; table-layout: fixed;
border-collapse: separate; border-spacing: 0; }
.first-col { width: var(--first-col); }
caption { padding: 8px 12px; text-align: left; font-weight: 700; background: white; }
th, td { padding: 10px 12px; border-right: 1px solid #c3d1d7;
border-bottom: 1px solid #c3d1d7; text-align: right; white-space: nowrap; background: white; }
thead th { position: sticky; top: 0; z-index: 2; background: #176f80; color: white; }
tbody th { position: sticky; left: 0; z-index: 1; background: #e8f1f5; text-align: left; white-space: normal; overflow-wrap: anywhere; }
thead th:first-child { left: 0; z-index: 3; text-align: left; }
tfoot th, tfoot td { position: sticky; bottom: 0; z-index: 2;
background: #fff2cf; font-weight: 700; }
tfoot th:first-child { left: 0; z-index: 3; text-align: left; }
</style>
</head>
<body>
<h1>행·열 틀 고정 예제</h1>
<p>표 영역 안에서 가로·세로로 스크롤하세요.</p>
<div class="jb-table-scroll demo" tabindex="0" role="region" aria-label="월별 수량 예제 표">
<table class="jb-table-01">
<caption>월별 수량: 실습용 가상 데이터</caption>
<colgroup><col class="first-col"><col span="4"></colgroup>
<thead><tr><th scope="col">품목</th><th scope="col">1월</th><th scope="col">2월</th><th scope="col">3월</th><th scope="col">4월</th></tr></thead>
<tbody>
<tr><th scope="row">품목 01</th><td>11</td><td>13</td><td>15</td><td>17</td></tr>
<tr><th scope="row">품목 02</th><td>12</td><td>14</td><td>16</td><td>18</td></tr>
<tr><th scope="row">품목 03</th><td>13</td><td>15</td><td>17</td><td>19</td></tr>
<tr><th scope="row">품목 04</th><td>14</td><td>16</td><td>18</td><td>20</td></tr>
<tr><th scope="row">품목 05</th><td>15</td><td>17</td><td>19</td><td>21</td></tr>
<tr><th scope="row">품목 06</th><td>16</td><td>18</td><td>20</td><td>22</td></tr>
<tr><th scope="row">품목 07</th><td>17</td><td>19</td><td>21</td><td>23</td></tr>
<tr><th scope="row">품목 08</th><td>18</td><td>20</td><td>22</td><td>24</td></tr>
<tr><th scope="row">품목 09</th><td>19</td><td>21</td><td>23</td><td>25</td></tr>
<tr><th scope="row">품목 10</th><td>20</td><td>22</td><td>24</td><td>26</td></tr>
<tr><th scope="row">품목 11</th><td>21</td><td>23</td><td>25</td><td>27</td></tr>
<tr><th scope="row">품목 12</th><td>22</td><td>24</td><td>26</td><td>28</td></tr>
</tbody>
<tfoot><tr><th scope="row">합계</th><td>198</td><td>222</td><td>246</td><td>270</td></tr></tfoot>
</table>
</div>
<p>Tab으로 표 영역에 이동한 뒤 방향키로도 스크롤할 수 있습니다. 표 밖의 문서 스크롤과 구분해 확인하세요.</p>
</body>
</html>
표 안에서 아래로 스크롤해도 청록색 열 제목이 위쪽에, 노란 합계 행이 아래쪽에 남습니다. 오른쪽으로 스크롤해도 품목 이름과 왼쪽 위의 품목 셀, 왼쪽 아래 합계 셀은 왼쪽에 남습니다. 스크롤을 되돌리면 각 셀의 원래 위치를 함께 확인할 수 있습니다. 페이지 자체를 스크롤하는 것과 표 영역 내부를 스크롤하는 것은 다른 동작입니다.
열 제목은 scope="col", 품목 이름은 scope="row"로 표시했습니다. caption은 표가 다루는 내용을 설명합니다. 시각적으로 고정해 놓는 것만으로 행·열의 의미 관계가 만들어지는 것은 아니므로 실제 table·th·td 구조를 유지해야 합니다. 표 머리글의 연결은 W3C WAI 표 머리글 안내를 참고할 수 있습니다.
행만 또는 열만 고정하고 겹침 해결하기
첫 행의 셀은 top: 0으로 고정하고 본문 첫 열은 left: 0으로 고정합니다. 전체 예제의 본문 첫 열 셀은 z-index: 1, 위아래 고정 행은 2, 두 방향이 겹치는 모서리는 3입니다. 배경을 투명하게 두면 뒤쪽 숫자가 비쳐 보일 수 있어 각 고정 영역에 불투명 배경색도 지정했습니다.
/* 첫 행 */
thead th { position: sticky; top: 0; z-index: 2; }
/* 본문 첫 열 */
tbody th { position: sticky; left: 0; z-index: 1; }
/* 첫 행과 첫 열의 교차 셀 */
thead th:first-child { left: 0; z-index: 3; }
가로 고정 없이 행만 남기려면 전체 예제에서 tbody th의 position: sticky·left 선언과 머리글·합계 첫 셀의 left 선언을 제거합니다. 마지막 합계 행도 고정하지 않으려면 tfoot 셀의 sticky·bottom 선언을 제거하세요. 반대로 열만 남기려면 thead·tfoot의 위아래 고정을 해제하되 각 첫 셀의 가로 sticky와 left: 0은 유지해야 합니다. 비교 후 원래 규칙으로 복원합니다.
z-index는 서로 다른 쌓임 맥락을 무조건 넘어서는 전역 순위가 아닙니다. 전체 예제는 스크롤 영역에 isolation: isolate를 지정해 표의 고정 셀 순서를 영역 안에서 관리합니다. 페이지의 메뉴·대화상자와 충돌한다고 숫자만 계속 높이기 전에 조상 요소가 만드는 쌓임 맥락을 확인하세요.
두 번째 열과 여러 머리글 행을 고정할 때
두 열을 고정할 때 둘째 열의 left는 첫 열의 실제 바깥 너비만큼 띄워야 합니다. 두 열 모두 left: 0으로 놓으면 서로 겹칩니다. 전체 예제는 첫 열의 너비를 --first-col: 120px와 colgroup으로 정했으므로 다음 코드를 style 끝에 추가해 둘째 열을 비교할 수 있습니다.
tbody td:nth-child(2) {
position: sticky;
left: var(--first-col);
z-index: 1;
background: #e8f1f5;
}
thead th:nth-child(2), tfoot td:nth-child(2) {
left: var(--first-col);
z-index: 3;
}
첫 열이 줄바꿈·반응형 너비 때문에 달라지면 둘째 열의 고정 위치도 그 실제 너비와 함께 바뀌어야 합니다. 좁은 화면에서 고정 열이 대부분의 가로 공간을 차지하면 나머지 데이터를 읽기 어려우므로 모바일에서는 첫 열만 남기는 등 표시 기준을 정하세요. 실습을 마치면 추가 규칙을 제거하면 됩니다.
머리글이 두 행이면 둘째 머리글 행의 top은 첫 머리글 행의 실제 높이만큼 내려야 합니다. 모든 행에 top: 0을 주면 머리글끼리 겹칩니다. 콘텐츠·글자 확대에 따라 높이가 바뀌는 표에서는 고정 픽셀 값을 추측해 넣지 말고 행 높이와 오프셋을 일관되게 관리하거나 해당 구조에 맞는 별도 구현을 검토해야 합니다.
고정이 안 되거나 선·내용이 가려질 때
먼저 position 계산값과 top·left·bottom 기준을 확인하고, 실제로 표 영역에 넘치는 내용이 있는지 봅니다. 다음으로 조상 overflow와 스크롤 컨테이너, 고정 셀의 배경·쌓임 순서를 확인합니다. 이 예제는 셀 테두리를 따로 그리는 border-collapse: separate와 border-spacing: 0을 사용했습니다. border-collapse: collapse인 표에서 선이 가려져 보인다면 별도 테두리 방식과 비교해 보세요.
스크롤 영역에 tabindex="0"과 접근 가능한 이름을 주어 키보드로 영역을 선택한 뒤 방향키로 이동할 수 있게 했습니다. 포커스 외곽선을 지우지 말고 실제 대상 브라우저에서 가로·세로 이동, Tab으로 영역을 빠져나오기, 모바일 터치 스크롤과 글자 확대 시 표시를 확인하세요. 표가 커질수록 고정 셀이 내용과 조작을 과도하게 가리지 않는지도 중요합니다.
이 방식은 한 행의 머리글과 첫 열의 행 제목을 가진 단순 표를 대상으로 합니다. 병합 셀·여러 머리글 단계·여러 고정 열이 있는 표는 셀의 의미 연결과 위치를 별도로 설계해야 합니다. 표 구조와 표시 방식의 기본은 MDN table 문서에서 확인할 수 있습니다.





