Headless UI v2.0 for React
- Date
- Adam Wathan
- Jonathan Reinink
자신의 도구로 실제 무언가를 만들어보는 것만큼 개선점을 찾는 데 좋은 방법은 없습니다.
지난 몇 달 동안 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>
)
}
새로운 Field
와 Fieldset
컴포넌트는 네이티브 <fieldset>
엘리먼트처럼 비활성화 상태를 캐스케이드합니다. 따라서 한 번에 전체 컨트롤 그룹을 쉽게 비활성화할 수 있습니다:
Select a country to see the region field become enabled
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 data-active data-hover data-focus class="bg-indigo-600 data-[active]:bg-indigo-700 data-[hover]:bg-indigo-500 data-[focus]:outline ...">
Submit
</button>
이러한 스타일을 JavaScript로 적용하는 것이 왜 중요한지 더 알아보려면, Devon Govett의 훌륭한 블로그 시리즈를 읽어보는 것을 강력히 추천합니다:
- 버튼 만들기 Part 1: Press Events
- 버튼 만들기 Part 2: Hover Interactions
- 버튼 만들기 Part 3: Keyboard Focus Behavior
웹은 정말 멋진 것을 만들기 위해 얼마나 많은 노력이 필요한지 항상 놀라게 만듭니다.
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>
)
}
새로운 가상 스크롤링 문서를 확인하여 더 알아보세요.
새로운 웹사이트와 개선된 문서
이번 주요 릴리스와 함께, 문서를 크게 개편하고 웹사이트도 새롭게 단장했습니다:
새로워진 headlessui.com을 방문해 확인해 보세요!