의사 클래스 :has()로 자식 요소에 따라 CSS 적용하기

회고 · 2025. 4. 24.

배경

React와 Tailwind CSS를 기반으로 LLM을 이용한 챗봇 프로젝트를 진행하면서, 메세지 입력 필드(Input 또는 Textarea)에 포커스 했을 때 단순히 입력 필드 자체 뿐만이 아니라 이를 감싸는 컨테이너 전체에 포커스를 주는 UI를 구현할 필요가 있었습니다.

사실 매우 간단한 과제이지만, 여러 방법을 정리해보면서 각 방식에 대한 장단점을 파악할 수 있었기에 이를 정리하고 공유합니다.

방법 1 : absolute position

function Input() {
  return (
    <Textarea
      className="relative flex flex-col ... focus:ring-1 focus:ring-offset-0"
      ...
    >
      {messages.length === 0 && (
        <div className="absolute top-0 left-0 w-full">
          <SuggestedActions />
        </div>
      )}

      <div className="absolute bottom-0 left-0 w-full flex justify-end p-2">
        {status === "submitted" ? (
          <StopButton />
        ) : (
          <SendButton />
        )}
      </div>
    </Textarea>
  );
}

처음 떠올릴 수 있는 방법 중 하나는 입력 필드를 최상위 요소로 두고, 그 안에 다른 부가적인 요소들(예: 제안 버튼, 전송/정지 버튼)을 absolute 포지셔닝을 이용해 배치할 수 있습니다

장점

  • 간단함

단점

  • 코드만으로 UI를 파악하기 어려움
  • 하위 요소가 복잡해질 경우 레이아웃 관리가 까다로움

방법 2 : 자식 속성에 따라 CSS적용하기

function Input() {
  return (
    <div className="flex flex-col ... has-focus:ring-1 has-focus:ring-offset-0">
      {messages.length === 0 && <SuggestedActions />}

      <Textarea
        className="..."
        ...
      />

      <div className="flex w-full justify-end p-2">
        {status === "submitted" ? (
          <StopButton />
        ) : (
          <SendButton />
        )}
      </div>
    </div>
  );
}

부모 div 요소안에 입력필드와 주변 요소를 배치하고, CSS 의사 클래스 :has()를 적용하면 자식 요소가 특정 조건을 만족하는 경우 스타일을 적용할 수 있습니다. 테일윈드에서는 이를 has-* 클래스명으로 적용할 수 있습니다. 따라서, has-focus:로 자식요소가 포커스 받는 경우에 링 스타일을 적용해줍니다.

만약 자식 요소가 아니라 형제 요소에 따라 스타일을 적용하고 싶다면 다음과 같이 peer 클래스명과 peer-has-* 클래스명을 사용할 수 있습니다. 부모 요소에 따라 스타일을 적용하고 싶다면 group 클래스명과 group-has-* 클래스명을 사용할 수 있습니다.

<div>
  <label class="peer ...">
    <input type="checkbox" name="todo[1]" checked />
    Create a to do list
  </label>
  <svg class="peer-has-checked:hidden ..."><!-- ... --></svg>
</div>
<a href="#" class="group ...">
  <div>
    <svg class="stroke-sky-500 group-hover:stroke-white ..." fill="none" viewBox="0 0 24 24">
      <!-- ... -->
    </svg>
    <h3 class="text-gray-900 group-hover:text-white ...">New project</h3>
  </div>
  <p class="text-gray-500 group-hover:text-white ...">Create a new project from a variety of starting templates.</p>
</a>

참고자료 1 : Tailwind CSS : hover-focus-and-other-states #has

:has() 의사 클래스는 CSS Selectors Level 4에 제안되었으며, 비교적 최근(2023년)에 주요 브라우저에서 지원이 시작되었습니다.

장점

  • 가장 정석적인 방법

단점

  • IE같은 구형 브라우저 지원 X

방법 3 : 상태로 구현하기

import { useState} from 'react';
import clsx from 'clsx';

function Input() {
    const [isFocused, setIsFocused] = useState(false);
  return (
    <div className={clsx("...", {
        isFocused : "ring-1 ring-offset-0"
    }>
      {messages.length === 0 && <SuggestedActions />}

      <Textarea
        className="..."
        onFocus={()=> setIsFocused(true)}
        onBlur={()=> setIsFocused(false)}
        ...
      />

      <div className="flex w-full justify-end p-2">
        {status === "submitted" ? (
          <StopButton />
        ) : (
          <SendButton />
        )}
      </div>
    </div>
  );
}

같은 UI를 자바스크립트(리액트)를 사용해서 구현할 수 있습니다. onFocusonBlur에 이벤트에 따라 상태를 변경하고, 이에 따라 조건부로 스타일을 적용합니다.

장점

  • 간단함
  • IE 지원

단점

  • 불필요한 자바스크립트

결론

2번 방법을 선택하였다.

후기

CSS의 매력에 스며들게 되었다. 의사 클래스를 더 사용할 기회가 있으면 좋겠다.