라디오버튼과 CSS로 만드는 다단 풀다운 메뉴

모든 웹사이트에 기본적으로 하나씩은 있는 풀다운 메뉴 구조를 CSS로 만들어 보겠습니다.

가로로 길게 펼쳐진 1단 메뉴는 목록 태그(<ul>)로 간단하게 만들 수 있지만, 2단, 또는 3단으로 펼쳐지는 메뉴를 만들려면 사용자가 메뉴를 펼치는 동작을 통해 메뉴 항목을 최종적으로 선택하는 과정을 필요로 합니다.

사용자가 하위 메뉴를 펼치는 과정은 마우스 호버, 또는 클릭을 통해 이루어집니다. 그리고 메뉴를 펼친 상태가 유지되어야 합니다. 그래야 더 하위의 메뉴를 펼칠 수 있습니다.

다단 풀다운 메뉴를 구현하는 많은 자바스크립트 라이브러리들이 있지만, 여기서는 자바스크립트 없이 CSS만으로, 그리고 사용자가 메뉴를 선택하는 동작은 라디오버튼과 체크박스를 이용해 구현합니다.

완성된 다단 풀다운 메뉴는 다음처럼 동작합니다.

HTML 구조는 다음과 같습니다.

샘플용 데이터 일부는 빠져있는 풀다운 메뉴 구조만입니다.

가로로 펼쳐진 1차 메뉴는 라디오버튼으로 선택 메뉴 상태를 구현하는 "input + label + .content" 구조를 사용합니다.

2차 메뉴는 ".content" 블록 요소 안에 목록으로 세로 메뉴를 구현합니다.

그리고 마지막 3차 메뉴는 2차 메뉴의 메뉴 1개를 다시 확장해서 "input + label + .subment" 구조로 확장을 합니다.

같은 방식으로 만들면 4차 메뉴도 만들 수 있습니다.

메뉴 항목이 아닌 컨텐츠 내용을 메뉴에 표시하는 경우에는 "<article></article>" 태그를 사용해 태그로 메뉴 영역에 표시하는 대상이 컨텐츠임을 구분할 수 있도록 합니다.

        <ul class="menus">
            <li class="menu">
                <input type="radio" id="menu-1" name="menu-group-1" />
                <label for="menu-1">다단 풀다운 메뉴</label>
                <div class="content">
                    <ul><!-- "2차 하위 메뉴" -->
                        <li><a href="#">메뉴 #1</a></li>
                        <li><a href="#">메뉴 #2</a></li>
                        <li>
                            <input type="checkbox" id="submenu-1" />
                            <label for="submenu-1">서브메뉴 펼침 #3</label>
                            <div class="submenu"><!-- "3차 하위 메뉴" -->
                                <ul>
                                    <li><a href="#">하위 메뉴 #1</a></li>
                                    <li><a href="#">하위 메뉴 #2</a></li>
                                    <li><a href="#">하위 메뉴 #3</a></li>
                                    <li><a href="#">하위 메뉴 #4</a></li>
                                </ul>
                            </div>
                        </li>
                        <li><a href="#">메뉴 #4</a></li>
                        <li><a href="#">메뉴 #5</a></li>
                    </ul>
                </div>
            </li>
            
            <li class="menu">
                <input type="radio" id="menu-2" name="menu-group-1" />
                <label for="menu-2">이미지 메뉴</label>
                <div class="content">
                    <ul>
                        <li><a href="#">메뉴 #1</a></li>
                        <li><a href="#">메뉴 #2</a></li>
                        <li><a href="#"><img src="./backup/879/img/cat.jpg"></a></li>
                        <li><a href="#">메뉴 #3</a></li>
                        <li><a href="#">메뉴 #4</a></li>
                    </ul>                
                </div>
            </li>
            
            <li class="menu">
                <input type="radio" id="menu-3" name="menu-group-1" />
                <label for="menu-3">컨텐츠</label>
                <div class="content">
                    <article>
                    <!-- "컨텐츠 표시" -->
                    </article>
                </div>
            </li>
        </ul>

풀다운 메뉴를 구현하는 전체 CSS는 다음과 같습니다.

풀다운 메뉴는 3차 하위 메뉴까지 구현되어 있습니다.

1차 메뉴는 ".menus" 클래스로, 2차 메뉴는 ".menu" 클래스로, 마지막 3차 메뉴는 ".submenu" 클래스를 기준으로 구현합니다.

CSS를 구현할 때 ".menus ul li ul li" 와 같은 접근 방식으로 3차 메뉴를 접근하는 방식도 가능하지만, 이렇게 여러 뎁스(Depth)로 CSS 선택자를 사용해서 접근 구조를 복잡하게 가져가는 것은 피하는 것이 좋습니다.

/* 1st row menu*/
.menus {
    position: relative;
    clear: both;
    margin: 0;
    padding: 0;
    list-style: none;
}
.menus [type="radio"]:checked + label + .content { /* 선택한 1차 메뉴의 2차 하위메뉴 표시 */
    z-index: 1;
    display: block;
}

/* 2nd vertical menu */
.menu { /* 2차 하위메뉴 구현 클래스 */
    float: left;
    position: relative;
}
.menu > label {
    background-color: #f0f0f0;
    padding: 0.5em 3.5em 0.5em 2em;
    cursor: pointer;
    text-align: center;
    display: block;
    position: relative;
}
.menu > label::after {
    content: "▼";
    font-style: normal;
    color: #aaa;
    position: absolute;
    margin-left: 0.5em;
    line-height: 1.3;
}
.menu input {
    display: none; /* 라디오버튼, 체크박스 감춤 */
}
.menu .content {
    position: absolute;
    top: 100%;
    display: none;
    left: 0;
    background: #333;
    color: #fff;
    padding: 20px;
}
.menu .content ul { /* 2차 메뉴 목록 불릿 및 패딩 정리 */
    margin: 0;
    padding: 0;
    list-style: none;
}
.menu .content img{
    max-width: 100%; /* 2차 메뉴 항목에 표시되는 이미지 크기 제한 */
}
.menu .content article{ /* 2차 메뉴 영역에 메뉴 대신 컨텐츠 표시용 태그 크기 제한 */
    min-width: 360px;
    max-height: 360px;
    overflow-y: auto;
}
.menu .content a, .menu .content input + label {
    display: block;
    color: #fff;
    white-space: nowrap;
    text-decoration: none;
    padding: 5px;
    cursor: pointer;
}
.menu .content input + label{
    padding-right: 24px;
}
.menu .content ul li:nth-child(n + 2){ /* 2차 하위 메뉴 항목 구분선 */
    border-top: 1px solid #999;
}
.menu [type="radio"]:checked + label { /* 1차 메뉴 선택 */
    background-color: #333;
    color: #fff;
    z-index: 2;
}
.menu > [type="radio"]:checked + label::after {
    content: "▲";
}  

/* 3rd vertical submenu */
.submenu{ /* 3차 하위 메뉴 클래스 */
    display: none;
    position: absolute;
    background-color: #333;
    padding: 20px;
    left: 100%; /* 3차 하위 메뉴 위치 조정 */
    margin: -16px 0 0 -16px; /* 3차 하위 메뉴 위치 조정 */
}
.menu > .content input:checked + label + .submenu { /* 3차 하위 메뉴 펼침 */
    z-index: 1;
    display: block;
}
.menu > .content > ul > li > input + label::after {
    content: "▶";
    position: absolute;
    margin-left: 0.5em;
    line-height: 1.2;
}
.menu > .content > ul > li > input:checked + label::after {
    content: "◀";
}

장식적인 구현 요소 제거

CSS가 다소 길어보이지만, 장식적인 부분을 위한 CSS를 제외하고 나면 구조는 상당히 단순합니다.

먼저 가상 요소인 "::after" 를 정의한 클래스 메뉴 펼침을 표시하기 위한 화살표 방향(▶◀▼▲))변경을 위한 것들입니다. 장식적인 요소이며, 메뉴가 펼쳐지는데 따라 화살표 방향이 바뀌는 반응형 동작이 필요 없으면 "::after" 를 정의한 클래스는 삭제해도 무방합니다.

가상 요소에 사용한 방향 화살표는 HTML 소스에 "<i>▶</i>" 와 같이 표현해서 내용을 바꾸는 방식으로 구현을 하는 것도 가능합니다.

다만 이렇게 구현할 경우 펼침 상태인 1차 메뉴 항목을 중복해서 만들어야 하기 때문에 HTML 내용이 늘어나고 관리하기도 번거로워 지는 문제가 있습니다.

HTML 태그로 화살표 변경을 구현하는 코드는 다음과 같습니다.

".close-menu" 클래스로 정의한 블록 태그("<div>")를 메뉴가 선택되면 앞의 라벨 태그 위에 표시되도록 해서 메뉴가 선택된 것을 표시하게 됩니다.

<label for="menu-1">다단 풀다운 메뉴 <i>▼</i></label>
<div class="close-menu">
  <input type="radio" id="menu-close" name="menu-group-1" />
  <label for="menu-close">다단 풀다운 메뉴 <i>▲</i></label>
</div>
.close-menu {
    position: absolute;
    z-index: -1;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}
.close-menu label {
    background: #333;
    color: white;
}
.menus [type="radio"]:checked ~ label ~ .close-menu {
    z-index: 3;
}

메뉴를 구현하는 방식에 따른 차이겠지만, 가상 요소를 사용하지 않고 HTML 태그와 일반 CSS 만으로도 가상요소로 구현하는 것과 같은 기능을 하는 메뉴를 구현할 수 있습니다.

메뉴에 표시하는 이미지 크기 처리

메뉴에 이미지를 붙여서 메뉴 항목을 표시하거나, 배너 이미지로 링크를 거는 경우 메뉴 항목 영역("<li>")에 이미지 태그("<img>")를 붙이게 됩니다.

이미지를 붙이게 되면 이미지 크기가 메뉴의 너비에 영향을 미치게 되기 때문에 이미지 크기를 적절히 제한해야 할 필요가 있습니다.

가장 단순하게는 이미지 너비를 픽셀 값으로 제한하는 것입니다. 명시적으로 크기가 제한되기 때문에 메뉴 너비 또한 그에 따라서 정해집니다.

.menu .content img{
    max-width: 200px;
}

또는 메뉴의 너비에 맞춰서 이미지 크기가 정해지도록 다음 처럼 크기를 제한할 수도 있습니다.

이때 100% 너비의 기준은 1차 메뉴의 메뉴 항목 너비가 됩니다.

.menu .content img{
    max-width: 100%;
}

3차 메뉴의 위치 정하기

2차 메뉴는 1차 메뉴 밑에 붙어서 표시가 되기 때문에 위치에 대한 고민이 없습니다.

3차 메뉴, 또는 그 이하 메뉴는 절대 좌표로 2차 메뉴의 표시 위치와 연동되어서 표시가 되기 때문에 CSS로 3차 메뉴의 위치를 정할 때는 속성 선택에 주의를 해야 합니다.

3차 메뉴의 기본 CSS는 다음과 같습니다. "position: absolute;" 속성으로 2차 메뉴 위에 띄워서 표시를 합니다.

.submenu{
    display: none;
    position: absolute;
    background-color: #333;
    padding: 20px;
}

3차 메뉴의 위치를 별도로 조정하지 않으면 기본적으로 다음과 같이 2차 메뉴의 해당 메뉴 항목 바로 밑에 겹쳐져 표시됩니다.

가장 쉽게 구현하는 방법이 2차 메뉴의 위치를 기준으로 오른쪽 아래 방향으로 절대 좌표를 지정하는 것입니다.

.submenu{
    display: none;
    position: absolute;
    background-color: #333;
    padding: 20px;
    top: 100px;
    left: 180px;
}

이렇게 구현하면 빠르고 간편하게 위치를 잡을 수 있지만, 2차 메뉴의 순서가 바뀌거나, 2차 메뉴가 더 넓어지거나 좁아지면 위치를 다시 저정해야 합니다.

2차 메뉴의 크기 변경이나, 메뉴 순서 변경에도 영향을 받지 않도록 하려면 다음과 같이 위치를 정해야 합니다.

.submenu{
    display: none;
    position: absolute;
    background-color: #333;
    padding: 20px;
    left: 100%; /* 2차 메뉴 오른쪽 끝에 3차 메뉴를 위치 */
    margin: -16px 0 0 -16px; /* 2차 메뉴 항목 왼쪽 위로 16px씩 3찰 메뉴를 이동 */
}

"left: 100%;" 는 왼쪽에서 "100%" 너비만큼 오른쪽으로 3차 메뉴를 이동시킵니다.

여기서 100%는 CSS 상속 규칙에 따라 2차 메뉴의 너비가 됩니다. 따라서 2차 메뉴 너비만큼 이동한 위치부터 3차 메뉴가 표시됩니다. 이렇게 하면 2차 메뉴의 너비 변경에 영향을 받지 않게 됩니다.

참고로 2차 메뉴의 너비는 고정이 아닙니다. 2차 메뉴에 표시되는 메뉴 항목 중 최대 길이를 가지는 메뉴항목의 너비가 2차 메뉴의 너비로 정해집니다.

세로 위치는 2차 메뉴 항목 위치를 기준으로 마진 값을 마이너스 값으로 설정해서 상대 위치를 기준으로 위로 이동을 시켜줍니다. 왼쪽 위로 "16px" 만큼 마이너스 마진을 지정해서 위치를 이동시켜주면 정확하게 원하는 위치에 3차 메뉴가 위치하게 됩니다.

이렇게 하면 2차 메뉴의 변경 사항과 무관하게 정확한 위치에 3차 메뉴 위치가 자동으로 정해지게 됩니다.

완성된 샘플 소스는 다음 압축 파일 링크를 클릭해 다운로드 받을 수 있습니다.

checkbox4.zip0.00MB