Headless UI v2.0 for React

Date
Headless UI v2.0

자신의 도구로 실제 무언가를 만들어보는 것만큼 개선점을 찾는 데 좋은 방법은 없습니다.

지난 몇 달 동안 Catalyst를 작업하면서, Headless UI에 수십 가지 개선 사항을 적용해 더 적은 코드를 작성하고 개발자 경험을 더욱 향상시켰습니다.

이 모든 작업의 결과물로 React용 Headless UI v2.0을 출시했습니다.

주요 새로운 기능은 다음과 같습니다:

@headlessui/react의 최신 버전을 npm에서 설치해 프로젝트에 추가하세요:

npm install @headlessui/react@latest

v1.x에서 업그레이드하는 경우, 변경 사항에 대해 자세히 알아보려면 업그레이드 가이드를 확인하세요.


내장된 앵커 포지셔닝

Floating UI를 Headless UI에 직접 통합했기 때문에, 드롭다운이 화면 밖으로 나가거나 다른 요소에 가려지는 걱정은 더 이상 하지 않아도 됩니다.

Menu, Popover, Combobox, Listbox 컴포넌트에 새로운 anchor prop을 사용해 앵커 포지셔닝을 지정하고, --anchor-gap--anchor-padding 같은 CSS 변수를 사용해 위치를 세밀하게 조정할 수 있습니다:

Scroll up and down to see the dropdown position change

import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'

function Example() {
  return (
      <Menu>
        <MenuButton>Options</MenuButton>
        <MenuItems
          anchor="bottom start"
          className="[--anchor-gap:8px] [--anchor-padding:8px]"
        >
          <MenuItem>
            <button>Edit</button>
          </MenuItem>
          <MenuItem>
            <button>Duplicate</button>
          </MenuItem>
          <hr />
          <MenuItem>
            <button>Archive</button>
          </MenuItem>
          <MenuItem>
            <button>Delete</button>
          </MenuItem>
        </MenuItems>
      </Menu>
  )
}

이 API의 장점은 sm:[--anchor-gap:4px] 같은 유틸리티 클래스를 사용해 CSS 변수를 변경함으로써 다양한 브레이크포인트에서 스타일을 조정할 수 있다는 점입니다.

자세한 내용은 각 컴포넌트의 앵커 포지셔닝 문서를 확인하세요.


새로운 체크박스 컴포넌트

기존의 RadioGroup 컴포넌트를 보완하기 위해 새로운 헤드리스 Checkbox 컴포넌트를 추가했습니다. 이를 통해 완전히 커스텀한 체크박스 컨트롤을 쉽게 만들 수 있습니다:

This will give you early access to any awesome new features we're developing.

import { Checkbox, Description, Field, Label } from '@headlessui/react'
import { CheckmarkIcon } from './icons/checkmark'
import clsx from 'clsx'

function Example() {
  return (
    <Field>
      <Checkbox
        defaultChecked
        className={clsx(
          'size-4 rounded border bg-white dark:bg-white/5',
          'data-[checked]:border-transparent data-[checked]:bg-blue-500',
          'focus:outline-none data-[focus]:outline-2 data-[focus]:outline-offset-2 data-[focus]:outline-blue-500',
        )}
      >
        <CheckmarkIcon className="stroke-white opacity-0 group-data-[checked]:opacity-100" />
      </Checkbox>
      <div>
        <Label>Enable beta features</Label>
        <Description>
          This will give you early access to any awesome new features we're developing.
        </Description>
      </div>
    </Field>
  )
}

체크박스는 제어되거나 제어되지 않을 수 있으며, HTML 폼과 잘 작동하도록 숨겨진 입력과 상태를 자동으로 동기화할 수 있습니다.

더 자세한 내용은 체크박스 문서를 참고하세요.


HTML 폼 컴포넌트

우리는 네이티브 폼 컨트롤을 감싸는 새로운 컴포넌트 세트를 추가했습니다. 이 컴포넌트들은 ID와 aria-* 속성을 자동으로 연결하는 번거로운 작업을 대신 처리해 줍니다.

이전에는 <label>과 설명이 적절히 연결된 간단한 <input> 필드를 만들기 위해 다음과 같은 코드를 작성해야 했습니다:

<div>
  <label id="name-label" for="name-input">이름</label>
  <input
    id="name-input"
    aria-labelledby="name-label"
    aria-describedby="name-description"
  />
  <p id="name-description">실제 이름을 사용하면 사람들이 당신을 알아볼 수 있습니다.</p>
</div>

이제 Headless UI v2.0의 새로운 컴포넌트를 사용하면 다음과 같이 간단하게 작성할 수 있습니다:

import { Description, Field, Input, Label } from '@headlessui/react'

function Example() {
  return (
    <Field>
      <Label>이름</Label>
      <Input name="your_name" />
      <Description>실제 이름을 사용하면 사람들이 당신을 알아볼 수 있습니다.</Description>
    </Field>
  )
}

새로운 FieldFieldset 컴포넌트는 네이티브 <fieldset> 엘리먼트처럼 비활성화 상태를 캐스케이드합니다. 따라서 한 번에 전체 컨트롤 그룹을 쉽게 비활성화할 수 있습니다:

Select a country to see the region field become enabled

Shipping details

We currently only ship to North America.

import { Button, Description, Field, Fieldset, Input, Label, Legend, Select } from '@headlessui/react'
import { regions } from './countries'

export function Example() {
  const [country, setCountry] = useState(null)

  return (
    <form action="/shipping">
      <Fieldset>
        <Legend>배송 정보</Legend>
        <Field>
          <Label>주소</Label>
          <Input name="address" />
        </Field>
        <Field>
          <Label>국가</Label>
          <Description>현재 북미 지역으로만 배송 가능합니다.</Description>
          <Select
            name="country"
            value={country}
            onChange={(event) => setCountry(event.target.value)}
          >
            <option></option>
            <option>캐나다</option>
            <option>멕시코</option>
            <option>미국</option>
          </Select>
        </Field>
        <Field disabled={!country}>
          <Label className="data-[disabled]:opacity-40">/</Label>
          <Select name="region" className="data-[disabled]:opacity-50">
            <option></option>
            {country && regions[country].map((region) => <option>{region}</option>)}
          </Select>
        </Field>
        <Button>제출</Button>
      </Fieldset>
    </form>
  )
}

우리는 렌더링된 HTML에서 data-disabled 속성을 사용해 비활성화 상태를 노출합니다. 이렇게 하면 <label> 엘리먼트처럼 네이티브 disabled 속성을 지원하지 않는 엘리먼트에서도 비활성화 상태를 쉽게 노출할 수 있습니다. 이를 통해 각 엘리먼트의 비활성화 스타일을 세밀하게 조정할 수 있습니다.

이번에 총 8개의 새로운 컴포넌트를 추가했습니다 — Fieldset, Legend, Field, Label, Description, Input, Select, 그리고 Textarea.

더 자세한 내용은 Fieldset 문서를 참고하고, 나머지 문서도 살펴보세요.


향상된 hover, focus, active 상태 감지

Headless UI는 이제 React Aria 라이브러리의 훅을 기반으로 더 스마트한 data-* 상태 속성을 컨트롤에 추가합니다. 이 속성들은 기본 CSS 의사 클래스보다 다양한 기기에서 더 일관된 동작을 보여줍니다:

  • data-active:active와 유사하지만, 요소에서 드래그를 멈추면 제거됩니다.
  • data-hover:hover와 유사하지만, 터치 기기에서는 스틱키 hover 상태를 방지하기 위해 무시됩니다.
  • data-focus:focus-visible와 유사하지만, 명령형 포커싱으로 인한 오탐지를 방지합니다.

Click, hover, focus, and drag the button to see the data attributes applied

<!-- Rendered `Button` -->
<button class="bg-indigo-600 data-[active]:bg-indigo-700 data-[hover]:bg-indigo-500 data-[focus]:outline ...">
  Submit
</button>

이러한 스타일을 JavaScript로 적용하는 것이 왜 중요한지 더 알아보려면, Devon Govett의 훌륭한 블로그 시리즈를 읽어보는 것을 강력히 추천합니다:

웹은 정말 멋진 것을 만들기 위해 얼마나 많은 노력이 필요한지 항상 놀라게 만듭니다.


Combobox 리스트 가상화

Headless UI에 TanStack Virtual을 통합하여 수십만 개의 항목을 콤보박스에 넣어야 할 때 리스트 가상화를 지원합니다. 왜냐하면, 보스가 그렇게 하라고 했기 때문이죠.

새로운 virtual prop을 사용하여 모든 항목을 전달하고, ComboboxOptions 렌더 prop을 사용하여 개별 옵션의 템플릿을 제공합니다:

Open the combobox and scroll through the 1,000 options

import { Combobox, ComboboxButton, ComboboxInput, ComboboxOption, ComboboxOptions } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { useState } from 'react'

const people = [
  { id: 1, name: 'Rossie Abernathy' },
  { id: 2, name: 'Juana Abshire' },
  { id: 3, name: 'Leonel Abshire' },
  { id: 4, name: 'Llewellyn Abshire' },
  { id: 5, name: 'Ramon Abshire' },
  // ...1000명까지
]

function Example() {
  const [query, setQuery] = useState('')
  const [selected, setSelected] = useState(people[0])

  const filteredPeople =
    query === ''
      ? people
      : people.filter((person) => {
          return person.name.toLowerCase().includes(query.toLowerCase())
        })

  return (
    <Combobox
      value={selected}
      virtual={{ options: filteredPeople }}
      onChange={(value) => setSelected(value)}
      onClose={() => setQuery('')}
    >
      <div>
        <ComboboxInput
          displayValue={(person) => person?.name}
          onChange={(event) => setQuery(event.target.value)}
        />
        <ComboboxButton>
          <ChevronDownIcon />
        </ComboboxButton>
      </div>
      <ComboboxOptions>
        {({ option: person }) => (
          <ComboboxOption key={person.id} value={person}>
            {person.name}
          </ComboboxOption>
        )}
      </ComboboxOptions>
    </Combobox>
  )
}

새로운 가상 스크롤링 문서를 확인하여 더 알아보세요.


새로운 웹사이트와 개선된 문서

이번 주요 릴리스와 함께, 문서를 크게 개편하고 웹사이트도 새롭게 단장했습니다:

새로운 Headless UI v2.0 웹사이트

새로워진 headlessui.com을 방문해 확인해 보세요!