Articles

마우스, 손가락, 움직임 줄이기 설정을 모두 배려하는 CSS

같은 페이지라도 마우스로 보는 사람, 손가락으로 보는 사람, 움직임을 줄여 둔 사람에게 다르게 보여야 해요. 이 사이트에서 미디어 쿼리로 나눈 기준을 정리했어요.

이 사이트는 작은 요소가 많고 움직임도 많아요. 커스텀 커서, 마우스를 올리면 빛나는 링크, 서서히 떠오르는 박스, 깜빡이는 로고 커서가 있어요. 데스크톱에서 만들 때는 다 괜찮았는데, 휴대폰으로 열어 보니 어색한 곳이 여럿 보였어요.

화면 너비만으로는 이 차이를 다 가를 수 없어요. 태블릿에 마우스를 꽂는 사람도 있고, 터치가 되는 노트북도 있어요. 그래서 너비 대신 입력 방식과 사용자 설정을 물어보는 미디어 쿼리를 썼어요.

hover 효과는 마우스가 있을 때만

휴대폰에서는 링크를 누르면 hover 상태가 남아 버리는 경우가 있어요. 손가락을 뗐는데도 링크가 노랗게 빛나고 있어서, 눌린 건지 아닌지 헷갈려요.

그래서 hover 스타일은 전부 hover: hover 안에 넣었어요. 마우스처럼 "올려 둘 수 있는" 입력 장치가 있을 때만 적용돼요.

@media (hover: hover) {
  .navbar-links a:hover,
  .site-footer-links a:hover,
  .articles-all:hover {
    color: var(--accent);
    text-shadow: 0 0 12px rgb(255 214 10 / 0.45);
  }

  .articles-item:hover .articles-name {
    color: var(--accent);
  }
}

hover 없이도 무엇을 누를 수 있는지는 알 수 있어야 해요. 그래서 hover는 꾸밈으로만 쓰고, 링크라는 것은 글자 색과 화살표로 이미 보이게 해 뒀어요.

커스텀 커서는 손가락에서 끄기

마우스 커서는 노란빛이 도는 작은 점 모양의 SVG예요. 파일을 따로 두지 않고 CSS 변수에 데이터 주소로 넣었어요.

:root {
  --cursor-default:
    url("data:image/svg+xml,...") 12 12,
    default;
  cursor: var(--cursor-default);
}

a {
  cursor: var(--cursor-pointer);
}

뒤에 붙은 12 12는 커서 이미지에서 실제로 클릭되는 점의 위치예요. 24픽셀짜리 원의 가운데예요. 이걸 빼면 왼쪽 위 모서리가 클릭 지점이 돼서, 보이는 곳과 눌리는 곳이 어긋나요. 마지막의 default는 이미지를 못 불러왔을 때 쓸 기본 커서예요.

터치 기기에서는 커서가 필요 없어요. 일부 기기는 마우스 커서를 화면에 그리려 하기도 해서, 손가락처럼 정밀하지 않은 입력 장치일 때는 원래대로 돌려놔요.

@media (pointer: coarse) {
  :root,
  a {
    cursor: auto;
  }
}

pointer: coarse는 주 입력 장치가 손가락처럼 넓은 영역을 누른다는 뜻이에요. 마우스나 펜은 fine이에요.

작은 링크는 누르는 영역을 키우기

프로젝트 박스 안에는 GitHub 같은 작은 링크가 있어요. 글자가 작아서 손가락으로 누르기 어려워요. 글자를 키우면 디자인이 무거워져서, 보이는 크기는 그대로 두고 누르는 영역만 넓혔어요.

.project-sublink::before {
  content: '';
  position: absolute;
  inset: -6px -4px;
}

@media (max-width: 600px) {
  .project-sublink::before {
    inset: -14px -10px;
  }
}

링크 위에 보이지 않는 가상 요소를 깔아요. 기본으로는 위아래 6픽셀, 좌우 4픽셀만 넓히고, 화면이 좁은 모바일에서는 위아래 14픽셀, 좌우 10픽셀까지 넓혀요. 넓은 화면에서는 마우스로 정확히 누를 수 있으니, 주변 요소를 덮지 않도록 조금만 넓혔어요.

움직임 줄이기를 켠 사람에게

운영체제에는 움직임을 줄이는 설정이 있어요. 화면이 움직이면 어지러움을 느끼는 사람들이 켜요. 이 설정을 켠 사람에게는 등장 애니메이션, 로고 커서 깜빡임, 전환 효과를 모두 꺼요.

@media (prefers-reduced-motion: reduce) {
  body::before,
  .navbar-brand,
  .project-card,
  .clock,
  .crew {
    animation: none;
  }

  .navbar-cursor {
    display: none;
  }
}

CSS만으로는 부족한 곳도 있어요. 로고가 한 글자씩 타이핑되는 효과는 자바스크립트 타이머로 돌아가요. 그래서 리액트 컴포넌트에서도 같은 설정을 확인하고, 켜져 있으면 처음부터 로고 전체를 보여줘요.

function prefersReducedMotion() {
  return window.matchMedia('(prefers-reduced-motion: reduce)').matches
}

const [count, setCount] = useState(() =>
  prefersReducedMotion() ? BRAND.length : 0,
)

배경의 별도 같아요. 이 설정이 켜져 있으면 반짝이지도 흐르지도 않는 별을 한 번만 그려요.

화면 가장자리와 주소창

요즘 휴대폰은 화면 모서리가 둥글고, 위쪽에 카메라 구멍이 있어요. 가로로 돌리면 글자가 그 아래로 숨기도 해요. 그래서 좌우 여백을 "16픽셀과 안전 영역 중 큰 쪽"으로 잡았어요.

.page {
  padding: clamp(24px, 9svh, 80px) max(16px, env(safe-area-inset-right))
    calc(48px + env(safe-area-inset-bottom))
    max(16px, env(safe-area-inset-left));
}

이 값이 동작하려면 index.html의 viewport 설정에 viewport-fit=cover가 있어야 해요.

위쪽 여백에는 svh를 썼어요. 휴대폰 브라우저는 주소창이 접히고 펴지면서 화면 높이가 바뀌는데, vh는 그때마다 값이 달라져서 레이아웃이 출렁여요. svh는 주소창이 펴져 있을 때의 작은 높이를 기준으로 해서 바뀌지 않아요.

320px까지 확인하기

가장 좁은 기준은 320픽셀이에요. 오래된 작은 휴대폰이나 화면을 반으로 나눠 쓰는 경우에 이 정도까지 좁아져요. 340픽셀 아래에서는 로고 글자와 박스 안쪽 여백을 한 번 더 줄여서, 가로 스크롤이 생기지 않게 했어요.

@media (max-width: 340px) {
  .navbar-brand {
    font-size: 9px;
  }

  .project-card {
    gap: 10px;
    padding: 14px;
  }
}

정리

  • hover 효과는 hover: hover 안에 두면 휴대폰에서 눌린 상태가 남지 않아요.
  • 커스텀 커서는 pointer: coarse일 때 원래대로 돌려요.
  • 작은 링크는 보이는 크기 대신 누르는 영역을 넓혀요.
  • 움직임 줄이기 설정은 CSS와 자바스크립트 양쪽에서 확인해요.
  • 안전 영역과 svh로 휴대폰의 모서리와 주소창에 대응해요.
다음 글 리액트 첫 화면을 미리 그려 두고 깜빡임 없이 이어받기