React Aria
uihooks-comboboxsource: ok · hit
ComboBox
npx shadcn add "https://react-aria.adobe.com/registry/hooks-combobox.json"free
Interaction
dropdown
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
https://react-aria.adobe.com/registry/hooks-dropdownlistbox.json
Files8 · source cached 10/8/2026, 5:08:23 PM
components/ui/ComboBox.tsx
'use client';
import {useComboBox} from 'react-aria/useComboBox';
import {useFilter} from 'react-aria/useFilter';
import {
useComboBoxState,
type ComboBoxProps as StatelyComboBoxProps
} from 'react-stately/useComboBoxState';
import {CollectionBuilder} from 'react-aria/CollectionBuilder';
import type {Collection as ICollection, Node} from '@react-types/shared';
import {Provider} from 'react-aria-components/slots';
import {ComboBoxStateContext} from 'react-aria-components/ComboBox';
import {Collection} from 'react-aria-components/Collection';
import {Label, LabelContext} from 'react-aria-components/Label';
import {Button, ButtonContext} from 'react-aria-components/Button';
import {Input, InputContext} from 'react-aria-components/Input';
import {OverlayTriggerStateContext} from 'react-aria-components/Dialog';
import {Popover, PopoverContext} from 'react-aria-components/Popover';
import {ListBoxContext, ListStateContext} from 'react-aria-components/ListBox';
import {ChevronDown} from 'lucide-react';
import {useRef} from 'react';
import type {ReactNode} from 'react';
import {DropdownItem, DropdownListBox} from '@/registry/react-aria/ui/DropdownListBox';
import './ComboBox.css';
import './Form.css';
import './ListBox.css';
import './Popover.css';
export {DropdownItem as ComboBoxItem};
export type ComboBoxProps = StatelyComboBoxProps<object> & {label?: ReactNode};
export function ComboBox(props: ComboBoxProps) {
return (
<CollectionBuilder content={<Collection {...props} />}>
{collection => <ComboBoxInner {...props} collection={collection} />}
</CollectionBuilder>
);
}
function ComboBoxInner({
collection,
...props
}: StatelyComboBoxProps<object> & {
collection: ICollection<Node<object>>;
label?: ReactNode;
}) {
let {contains} = useFilter({sensitivity: 'base'});
let state = useComboBoxState({
...props,
defaultFilter: contains,
collection,
children: undefined
});
let fieldRef = useRef<HTMLDivElement>(null);
let inputRef = useRef<HTMLInputElement>(null);
let buttonRef = useRef<HTMLButtonElement>(null);
let listBoxRef = useRef<HTMLDivElement>(null);
let popoverRef = useRef<HTMLDivElement>(null);
let {labelProps, inputProps, buttonProps, listBoxProps} = useComboBox(
{...props, inputRef, buttonRef, listBoxRef, popoverRef},
state
);
return (
<Provider
values={[
[ComboBoxStateContext, state],
[LabelContext, labelProps],
[ButtonContext, {...buttonProps, ref: buttonRef, isPressed: state.isOpen}],
[InputContext, {...inputProps, ref: inputRef}],
[OverlayTriggerStateContext, state],
[
PopoverContext,
{
ref: popoverRef,
triggerRef: fieldRef,
scrollRef: listBoxRef,
trigger: 'ComboBox',
placement: 'bottom start',
isNonModal: true
}
],
[ListBoxContext, {...listBoxProps, ref: listBoxRef}],
[ListStateContext, state]
]}>
<div className="react-aria-ComboBox">
<Label>{props.label}</Label>
<div className="combobox-field" ref={fieldRef}>
<Input className="react-aria-Input inset" />
<Button className="field-Button">
<ChevronDown />
</Button>
</div>
<Popover className="react-aria-Popover combobox-popover">
<DropdownListBox />
</Popover>
</div>
</Provider>
);
}
components/ui/ComboBox.css
@import './theme.css';
@import './TextField.css';
.react-aria-ComboBox {
display: flex;
flex-direction: column;
color: var(--text-color);
width: calc(var(--spacing) * 50);
.combobox-field {
display: flex;
align-items: center;
height: var(--spacing-8);
width: 100%;
}
.react-aria-Input {
width: 100%;
padding: 0 calc(var(--spacing-3) + var(--spacing-6)) 0 var(--spacing-3);
}
.react-aria-ComboBoxValue {
margin-top: var(--spacing-1);
font: var(--font-size-sm) system-ui;
color: var(--text-color);
}
}
.combobox-popover[data-trigger='ComboBox'] {
width: var(--trigger-width);
padding: 0;
}
components/ui/theme.css
/* colors */
:root {
/* Change this variable to set the theme color for all components. */
/* You can use the below presets, or choose a custom color. */
--tint: var(--indigo);
/* theme colors */
/* lightness channel is only used as a multiplier for chroma (see below) */
--gray: oklch(0.5 0 0);
--red: oklch(0.6 0.181447 27.0726);
--orange: oklch(0.7 0.150492 54);
--yellow: oklch(0.8 0.128516 73.8032);
--turquoise: oklch(0.5 0.081146 205.114);
--cyan: oklch(0.4 0.142107 243.926);
--green: oklch(0.5 0.121276 155.372);
--blue: oklch(0.5 0.22049 266.315);
--indigo: oklch(1 0.25049 284.23);
--purple: oklch(0.7 0.223324 302);
--pink: oklch(0.6 0.177717 347.813);
/** tint color scale */
--tint-100: oklch(from var(--tint) var(--lightness-100) var(--chroma-100) h);
--tint-200: oklch(from var(--tint) var(--lightness-200) var(--chroma-200) h);
--tint-300: oklch(from var(--tint) var(--lightness-300) var(--chroma-300) h);
--tint-400: oklch(from var(--tint) var(--lightness-400) var(--chroma-400) h);
--tint-500: oklch(from var(--tint) var(--lightness-500) var(--chroma-500) h);
--tint-600: oklch(from var(--tint) var(--lightness-600) var(--chroma-600) h);
--tint-700: oklch(from var(--tint) var(--lightness-700) var(--chroma-700) h);
--tint-800: oklch(from var(--tint) var(--lightness-800) var(--chroma-800) h);
--tint-900: oklch(from var(--tint) var(--lightness-900) var(--chroma-900) h);
--tint-1000: oklch(from var(--tint) var(--lightness-1000) var(--chroma-1000) h);
--tint-1100: oklch(from var(--tint) var(--lightness-1100) var(--chroma-1100) h);
--tint-1200: oklch(from var(--tint) var(--lightness-1200) var(--chroma-1200) h);
--tint-1300: oklch(from var(--tint) var(--lightness-1300) var(--chroma-1300) h);
--tint-1400: oklch(from var(--tint) var(--lightness-1400) var(--chroma-1400) h);
--tint-1500: oklch(from var(--tint) var(--lightness-1500) var(--chroma-1500) h);
--tint-1600: oklch(from var(--tint) var(--lightness-1600) var(--chroma-1600) h);
/* gray scale */
--gray-100: oklch(from var(--gray) var(--lightness-100) c h);
--gray-200: oklch(from var(--gray) var(--lightness-200) c h);
--gray-300: oklch(from var(--gray) var(--lightness-300) c h);
--gray-400: oklch(from var(--gray) var(--lightness-400) c h);
--gray-500: oklch(from var(--gray) var(--lightness-500) c h);
--gray-600: oklch(from var(--gray) var(--lightness-600) c h);
--gray-700: oklch(from var(--gray) var(--lightness-700) c h);
--gray-800: oklch(from var(--gray) var(--lightness-800) c h);
--gray-900: oklch(from var(--gray) var(--lightness-900) c h);
--gray-1000: oklch(from var(--gray) var(--lightness-1000) c h);
--gray-1100: oklch(from var(--gray) var(--lightness-1100) c h);
--gray-1200: oklch(from var(--gray) var(--lightness-1200) c h);
--gray-1300: oklch(from var(--gray) var(--lightness-1300) c h);
--gray-1400: oklch(from var(--gray) var(--lightness-1400) c h);
--gray-1500: oklch(from var(--gray) var(--lightness-1500) c h);
--gray-1600: oklch(from var(--gray) var(--lightness-1600) c h);
}
/* light mode colors */
:root {
--background-color: #f8f8f8;
--gray-50: #ffffff;
--lightness-100: 98.1187%;
--lightness-200: 95.2045%;
--lightness-300: 91.1434%;
--lightness-400: 85.1751%;
--lightness-500: 79.1773%;
--lightness-600: 72.3297%;
--lightness-700: 67.0121%;
--lightness-800: 62.3039%;
--lightness-900: 57.9699%;
--lightness-1000: 51.9076%;
--lightness-1100: 46.9058%;
--lightness-1200: 41.0821%;
--lightness-1300: 35.3616%;
--lightness-1400: 29.6725%;
--lightness-1500: 24.5366%;
--lightness-1600: 16.6959%;
/* lower chroma at low lightness levels */
--chroma-100: calc(l * c * 0.5);
--chroma-200: calc(l * c * 0.6);
--chroma-300: calc(l * c * 0.7);
--chroma-400: calc(l * c * 0.8);
--chroma-500: calc(l * c * 0.9);
--chroma-600: c;
--chroma-700: c;
--chroma-800: c;
--chroma-900: c;
--chroma-1000: c;
--chroma-1100: c;
--chroma-1200: c;
--chroma-1300: c;
--chroma-1400: c;
--chroma-1500: c;
--chroma-1600: c;
--highlight-hover: rgb(0 0 0 / 0.07);
--highlight-pressed: rgb(0 0 0 / 0.15);
--overlay-background: var(--gray-50);
--overlay-border: rgb(0 0 0 / 0.06);
--popover-shadow: 0 8px 20px rgba(0 0 0 / 0.12);
}
/* dark mode colors */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #1b1b1b;
--gray-50: oklch(22% 0 0);
--lightness-100: 29.6725%;
--lightness-200: 35.3616%;
--lightness-300: 41.0821%;
--lightness-400: 46.9058%;
--lightness-500: 51.9076%;
--lightness-600: 57.9699%;
--lightness-700: 56.1347%;
--lightness-800: 59.2866%;
--lightness-900: 62.3039%;
--lightness-1000: 67.0121%;
--lightness-1100: 72.3297%;
--lightness-1200: 79.1773%;
--lightness-1300: 85.1751%;
--lightness-1400: 91.1434%;
--lightness-1500: 95.2045%;
--lightness-1600: 100%;
--highlight-hover: rgb(255 255 255 / 0.1);
--highlight-pressed: rgb(255 255 255 / 0.2);
--overlay-background: var(--gray-100);
--overlay-border: rgb(255 255 255 / 0.2);
--popover-shadow: 0 8px 20px rgba(0 0 0 / 0.5);
}
}
/* Semantic colors */
:root {
--focus-ring-color: var(--tint-1000);
--text-color: var(--gray-1200);
--text-color-hover: var(--gray-1300);
--text-color-disabled: var(--gray-600);
--text-color-placeholder: var(--gray-1000);
--link-color: var(--tint-1200);
--link-color-secondary: var(--gray-1200);
--link-color-pressed: var(--tint-1300);
--border-color: var(--gray-400);
--border-color-hover: var(--gray-500);
--border-color-disabled: var(--gray-300);
--field-text-color: var(--gray-1400);
--button-background: var(--tint-100);
--button-background-pressed: var(--tint-200);
/* these colors are the same between light and dark themes
* to ensure contrast with the foreground color */
--highlight-background: oklch(from var(--tint) 55% c h);
--highlight-background-pressed: oklch(from var(--tint) 50% c h);
--highlight-background-invalid: oklch(from var(--red) var(--lightness-900) c h);
--highlight-foreground: white;
--highlight-overlay: oklch(from var(--tint-1000) l c h / 15%);
--invalid-color: oklch(from var(--red) var(--lightness-1000) c h);
--field-background: var(--gray-50);
}
/* Windows high contrast mode overrides */
@media (forced-colors: active) {
:root {
--background-color: Canvas;
--focus-ring-color: Highlight;
--text-color: ButtonText;
--text-color-hover: ButtonText;
--text-color-disabled: GrayText;
--text-color-placeholder: ButtonText;
--link-color: LinkText;
--link-color-secondary: LinkText;
--link-color-pressed: LinkText;
--border-color: ButtonBorder;
--border-color-hover: ButtonBorder;
--border-color-pressed: ButtonBorder;
--border-color-disabled: GrayText;
--field-background: Field;
--field-text-color: FieldText;
--overlay-background: Canvas;
--overlay-border: ButtonBorder;
--button-background: ButtonFace;
--button-background-pressed: ButtonFace;
--highlight-background: Highlight;
--highlight-background-pressed: Highlight;
--highlight-background-invalid: LinkText;
--highlight-foreground: HighlightText;
--invalid-color: LinkText;
}
}
/* typography and spacing scale */
:root {
--font-size: 0.875rem; /* 14px */
--font-size-sm: 0.75rem; /* 12px */
--font-size-lg: 1rem; /* 16px */
--radius: 8px;
--radius-sm: 6px;
--radius-lg: 10px;
--radius-xl: 16px;
--spacing: 0.25rem; /* 4px */
--spacing-1: var(--spacing);
--spacing-2: calc(2 * var(--spacing));
--spacing-3: calc(3 * var(--spacing));
--spacing-4: calc(4 * var(--spacing));
--spacing-5: calc(5 * var(--spacing));
--spacing-6: calc(6 * var(--spacing));
--spacing-7: calc(7 * var(--spacing));
--spacing-8: calc(8 * var(--spacing));
--spacing-9: calc(9 * var(--spacing));
--spacing-10: calc(10 * var(--spacing));
}
/* Scale up hit targets on high resolution mobile devices. */
@media (min-resolution: 200dpi) {
:root {
--spacing: calc(0.25rem * 1.25);
--font-size: 1.0625rem; /* 17px */
--font-size-sm: 0.9375rem; /* 15px */
--font-size-lg: 1.25rem; /* 20px */
}
}
components/ui/TextField.css
@import './theme.css';
@import './utilities.css';
.react-aria-TextField {
display: flex;
flex-direction: column;
}
.react-aria-Input,
.react-aria-TextArea {
width: 100%;
min-height: var(--spacing-8);
padding: 0 var(--spacing-3);
box-sizing: border-box;
margin: 0;
border: none;
border-radius: var(--radius);
font: var(--font-size) system-ui;
color: var(--field-text-color);
outline: none;
-webkit-tap-highlight-color: transparent;
&::placeholder {
color: var(--text-color-placeholder);
}
&[data-focused] {
outline: 2px solid var(--focus-ring-color);
outline-offset: -1px;
}
&[data-disabled] {
color: var(--text-color-disabled);
&::placeholder {
color: var(--text-color-disabled);
}
}
}
.react-aria-TextArea {
min-height: 64px;
padding: var(--spacing-2) var(--spacing-3);
}
components/ui/utilities.css
@layer utilities {
/* utility that creates a button-like element, which can be optionally selected */
.button-base {
--button-color: var(--tint);
--button-background: oklch(from var(--button-color) var(--lightness-100) var(--chroma-100) h);
--button-gradient: oklch(from var(--button-color) var(--lightness-200) var(--chroma-200) h);
--button-border: oklch(from var(--button-color) var(--lightness-300) var(--chroma-300) h);
--button-highlight: rgb(255 255 255 / 0.8);
--button-shadow: oklch(from var(--button-color) var(--lightness-400) var(--chroma-400) h);
--button-border-size: 1px;
--button-text: oklch(from var(--button-color) var(--lightness-1400) var(--chroma-1400) h);
--button-gradient-size: 8px;
background: var(--button-background);
color: var(--button-text);
box-shadow:
inset 0 -1px 0 var(--button-shadow),
/* bottom shadow */ inset 0 0 0 var(--button-border-size) var(--button-border),
/* border */ inset 0px calc(var(--button-border-size) + 1px) 0px var(--button-highlight),
/* top specular highlight */ inset 0px calc(-1 * var(--button-gradient-size))
var(--button-gradient-size) -2px var(--button-gradient); /* inner gradient */
outline: none;
transition-property: background, color, scale, box-shadow;
transition-duration: 200ms;
will-change: scale;
forced-color-adjust: none;
-webkit-tap-highlight-color: transparent;
@media (prefers-color-scheme: dark) {
--button-shadow: oklch(from var(--button-color) var(--lightness-200) var(--chroma-200) h);
--button-highlight: rgb(255 255 255 / 0.15);
box-shadow:
inset 0 var(--button-border-size) 0 var(--button-highlight),
/* top specular highlight */ inset 0 calc(-1 * var(--button-border-size)) 0
var(--button-shadow),
/* bottom shadow */ inset 0 0 0 var(--button-border-size) var(--button-border),
/* border */ inset 0 var(--button-gradient-size) var(--button-gradient-size) -2px
var(--button-gradient); /* inner gradient */
}
&:where([data-pressed]) {
--button-background: oklch(from var(--button-color) var(--lightness-200) var(--chroma-200) h);
}
&:where([data-focus-visible]) {
outline: 2px solid var(--focus-ring-color);
outline-offset: 2px;
}
&:where([data-variant='secondary']) {
--button-color: var(--gray);
}
&:where([data-variant='quiet']) {
--button-background: none;
--button-text: var(--text-color);
box-shadow: 0 0 0 1px transparent;
&:where([data-hovered], [data-pressed]) {
--button-background: var(--tint-200);
--button-text: var(--tint-1400);
box-shadow: 0 0 0 1px var(--tint-200);
}
}
&:where([data-selected]) {
--button-background: oklch(from var(--button-color) 55% c h);
--button-border: oklch(from var(--button-color) 50% c h);
--button-gradient: var(--button-border);
--button-highlight: rgb(255 255 255 / 0.2);
--button-shadow: oklch(from var(--button-color) 30% c h);
--button-text: var(--highlight-foreground);
box-shadow:
inset 0 -1px 0 var(--button-shadow),
/* bottom shadow */ inset 0 0 0 1px var(--button-border),
/* border */ inset 0 2px 0 var(--button-highlight),
/* top specular highlight */ inset 0 calc(-1 * var(--button-gradient-size))
var(--button-gradient-size) var(--button-gradient); /* inner gradient */
@media (prefers-color-scheme: dark) {
--button-highlight: rgb(255 255 255 / 0.4);
--button-gradient: rgb(255 255 255 / 0.2);
--button-shadow: var(--button-border);
box-shadow:
inset 0 1px 0 var(--button-highlight),
/* top specular highlight */ inset 0 var(--button-gradient-size)
var(--button-gradient-size) var(--button-gradient),
/* inner gradient */ inset 0 0 0 1px var(--button-border); /* border */
}
&:where([data-pressed]) {
--button-background: oklch(from var(--button-color) 50% c h);
}
}
&:where([data-disabled]) {
box-shadow: none;
--button-background: var(--border-color-disabled);
--button-text: var(--text-color-disabled);
&:where([data-variant='quiet']) {
--button-background: none;
}
}
@media (forced-colors: active) {
--button-background: ButtonFace;
--button-text: ButtonText;
--button-border: ButtonBorder;
box-shadow: inset 0 0 0 var(--button-border-size) var(--button-border);
&:where([data-variant='quiet']) {
--button-border: transparent;
&:where([data-hovered], [data-pressed]) {
--button-border: ButtonBorder;
}
}
&:where([data-selected]) {
--button-background: Highlight;
--button-text: HighlightText;
--button-border: Highlight;
}
&:where([data-disabled]) {
--button-background: ButtonFace;
--button-text: GrayText;
--button-border: GrayText;
&:where([data-variant='quiet']) {
--button-border: transparent;
}
}
}
}
/* utility that creates a small indicator, such as a checkbox, radio, switch, or slider thumb */
.indicator {
--indicator-color: var(--gray);
--indicator-background: oklch(
from var(--indicator-color) var(--lightness-100) var(--chroma-100) h
);
--indicator-border: oklch(from var(--indicator-color) var(--lightness-800) var(--chroma-800) h);
--indicator-drop-shadow: 0 0;
background: var(--indicator-background);
box-shadow:
inset 0 0 0 1px var(--indicator-border),
/* border */ inset 0 2px 0 white,
/* top specular highlight */ inset 0 -4px 2px
oklch(from var(--indicator-color) 30% c h / 0.08),
/* inner gradient */ var(--indicator-drop-shadow); /* optional drop shadow */
will-change: scale;
@media (prefers-color-scheme: dark) {
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.4),
/* top specular highlight */ inset 0 4px 2px rgb(255 255 255 / 0.1),
/* inner shadow */ inset 0 0 0 1px var(--indicator-border),
/* border */ var(--indicator-drop-shadow); /* optional drop shadow */
}
@media (forced-colors: active) {
--indicator-background: ButtonFace;
--indicator-border: ButtonBorder;
box-shadow: inset 0 0 0 1px var(--indicator-border);
}
&[data-pressed],
[data-pressed] & {
scale: 0.9;
}
[data-selected] > &,
[data-indeterminate] > & {
--indicator-color: var(--highlight-background);
--indicator-background: var(--indicator-color);
--indicator-highlight: rgb(255 255 255 / 0.3);
--indicator-shadow: oklch(from var(--indicator-color) 45% c h);
--indicator-border: var(--indicator-background);
box-shadow:
inset 0 -1px 0 var(--indicator-shadow),
/* bottom shadow */ inset 0 0 0 1px var(--indicator-border),
/* border */ inset 0 2px 0 var(--indicator-highlight),
/* top specular highlight */ var(--indicator-drop-shadow); /* optional drop shadow */
@media (prefers-color-scheme: dark) {
--indicator-highlight: rgb(255 255 255 / 0.5);
--indicator-gradient: rgb(255 255 255 / 0.12);
box-shadow:
inset 0 1px 0 var(--indicator-highlight),
/* top specular highlight */ inset 0 4px 2px var(--indicator-gradient),
/* inner gradient */ inset 0 0 0 1px var(--indicator-border),
/* border */ var(--indicator-drop-shadow); /* optional drop shadow */
}
@media (forced-colors: active) {
box-shadow: none;
}
}
&[data-invalid],
[data-invalid] > & {
--indicator-color: var(--invalid-color);
@media (forced-colors: active) {
--indicator-border: var(--invalid-color);
}
}
&[data-focus-visible],
[data-focus-visible] > & {
outline: 2px solid var(--focus-ring-color);
outline-offset: 2px;
}
&[data-disabled],
[data-disabled] > & {
background: var(--field-background);
box-shadow: inset 0 0 0 1px var(--border-color-disabled);
}
}
/* utility that creates an inset effect, used for form fields, slider/progress tracks, etc. */
.inset {
--inset-background: var(--field-background);
--inset-border: var(--border-color);
--inset-border-size: 1px;
--inset-shadow-offset: 2px;
--inset-shadow-size: 4px;
background: var(--inset-background);
box-shadow:
inset 0 0 0 var(--inset-border-size) var(--inset-border),
/* border */ inset 0 var(--inset-shadow-offset) var(--inset-shadow-size) rgb(0 0 0 / 0.15),
/* inner shadow */ 0 1px 0 var(--gray-50); /* bottom specular highlight */
transition: box-shadow 200ms;
forced-color-adjust: none;
@media (prefers-color-scheme: dark) {
--inset-border: var(--gray-200);
--border-color-hover: var(--gray-300);
--inset-highlight: var(--gray-400);
--inset-shadow-size: 6px;
box-shadow:
inset 0 calc(-1 * var(--inset-border-size)) 0 var(--inset-highlight),
/* bottom specular highlight */ inset 0 0 0 var(--inset-border-size) var(--inset-border),
/* border */ inset 0 1px var(--inset-shadow-size) rgb(0 0 0); /* inner shadow */
}
&:where([data-hovered], [data-pressed]) {
--inset-border: var(--border-color-hover);
}
@media (forced-colors: active) {
--inset-border: ButtonBorder;
box-shadow: inset 0 0 0 var(--inset-border-size) var(--inset-border);
}
&:where([data-invalid]) {
--inset-border: var(--invalid-color);
--inset-highlight: var(--inset-border);
}
&:where([data-disabled]) {
box-shadow: inset 0 0 0 1px var(--border-color-disabled);
}
&.track {
--inset-shadow-offset: 1px;
--inset-shadow-size: 3px;
@media (prefers-color-scheme: light) {
--inset-background: var(--gray-300);
--inset-border: var(--gray-500);
--inset-border-size: 0.5px;
}
@media (forced-colors: active) {
--inset-background: Field;
--inset-border: ButtonBorder;
--inset-border-size: 1px;
}
}
}
}
components/ui/Form.css
@import './theme.css';
.react-aria-Form {
display: flex;
flex-direction: column;
gap: var(--spacing-6);
> .react-aria-Button {
align-self: start;
}
}
.react-aria-Form [role='alert'] {
border: 2px solid var(--invalid-color);
background: var(--overlay-background);
border-radius: 6px;
padding: 12px;
max-width: 250px;
outline: none;
&:focus {
outline: 2px solid var(--focus-ring-color);
outline-offset: 2px;
}
h3 {
margin-top: 0;
}
p {
margin-bottom: 0;
}
}
.react-aria-Label {
display: block;
font: var(--font-size) system-ui;
color: var(--text-color);
margin-bottom: var(--spacing-2);
font-weight: 500;
cursor: default;
}
.react-aria-FieldError {
display: block;
font: var(--font-size-sm) system-ui;
color: var(--invalid-color);
margin-top: var(--spacing-1);
contain: inline-size;
}
.field-description {
display: block;
font: var(--font-size-sm) system-ui;
margin-top: var(--spacing-1);
contain: inline-size;
[data-invalid] > & {
display: none;
}
}
.field-Button {
background: var(--tint-200);
color: var(--tint-1400);
forced-color-adjust: none;
border-radius: var(--radius-sm);
border: none;
margin-left: calc(-1 * var(--spacing-8) + 4px);
height: calc(100% - 8px);
aspect-ratio: 1/1;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition-property: scale, background;
transition-duration: 200ms;
-webkit-tap-highlight-color: transparent;
svg {
width: var(--spacing-4);
height: var(--spacing-4);
}
&:where([data-hovered], [data-pressed]) {
background: var(--tint-300);
color: var(--tint-1200);
}
@media (forced-colors: active) {
background: Highlight;
color: HighlightText;
}
&[data-pressed] {
scale: 0.9;
}
&[data-focus-visible] {
outline: 2px solid var(--focus-ring-color);
outline-offset: 2px;
}
&[data-disabled] {
background: none;
color: var(--text-color-disabled);
}
}
components/ui/ListBox.css
@import './theme.css';
.react-aria-ListBox {
max-height: inherit;
overflow: auto;
border: 0.5px solid var(--border-color);
border-radius: calc(var(--radius) + var(--spacing-1));
background: var(--overlay-background);
forced-color-adjust: none;
outline: none;
box-sizing: border-box;
font: var(--font-size) system-ui;
color: var(--text-color);
&:has(.react-aria-ListBoxSection) {
scroll-padding: 24px;
}
&[data-focus-visible] {
outline: 2px solid var(--focus-ring-color);
outline-offset: -1px;
}
&[data-empty] {
align-items: center;
justify-content: center;
font-style: italic;
}
&[data-drop-target] {
outline: 2px solid var(--highlight-background);
outline-offset: -1px;
background: var(--highlight-overlay);
}
.react-aria-DropIndicator[data-drop-target] {
z-index: 3;
outline: 1px solid var(--highlight-background);
}
}
.react-aria-ListBoxSection {
.react-aria-Header {
position: sticky;
top: -0.5px;
z-index: 2;
font-size: var(--font-size-sm);
font-weight: 600;
font-variation-settings: initial;
padding: var(--spacing-1) var(--spacing-4);
background: var(--gray-100);
border-block: 0.5px solid var(--gray-400);
cursor: default;
user-select: none;
box-shadow:
inset 0px 1px 0px white,
inset 0px -4px 8px var(--gray-200);
@media (prefers-color-scheme: dark) {
box-shadow: inset 0px 4px 8px var(--gray-200);
}
}
&:first-child .react-aria-Header {
margin-top: -0.5px;
}
}
.react-aria-ListBoxItem {
padding: var(--spacing-1) var(--spacing-3);
border-radius: var(--radius);
outline: none;
cursor: default;
color: var(--text-color);
font: var(--font-size) system-ui;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
min-height: var(--spacing-8);
box-sizing: border-box;
position: relative;
transition-property: background, color, border-radius;
transition-duration: 200ms;
-webkit-tap-highlight-color: transparent;
--border-color: var(--gray-300);
&:not(:last-child)::after {
content: '';
display: block;
position: absolute;
}
&[data-pressed] {
background: var(--highlight-hover);
border-radius: var(--radius);
}
&[data-focus-visible] {
outline: 2px solid var(--focus-ring-color);
outline-offset: -2px;
z-index: 2;
}
&[data-selected] {
background: var(--highlight-background);
color: var(--highlight-foreground);
--border-color: transparent;
&[data-focus-visible] {
outline-color: var(--highlight-foreground);
outline-offset: -4px;
}
&[data-pressed] {
background: var(--highlight-background-pressed);
}
&:has(+ [data-selected]),
&:has(+ .react-aria-DropIndicator + [data-selected]) {
--border-color: rgb(255 255 255 / 0.3);
}
}
[slot='label'] {
font-weight: 500;
}
[slot='description'] {
font-size: var(--font-size-sm);
}
&[href] {
text-decoration: none;
cursor: pointer;
-webkit-touch-callout: none;
}
&[data-disabled] {
color: var(--text-color-disabled);
}
&[data-dragging] {
opacity: 0.6;
}
&[data-drop-target] {
outline: 2px solid var(--highlight-background);
background: var(--highlight-overlay);
}
}
.react-aria-ListBox {
&[data-layout='stack'] {
display: flex;
&[data-orientation='vertical'] {
flex-direction: column;
width: 250px;
max-height: 300px;
min-height: 100px;
.react-aria-ListBoxItem {
margin-inline: var(--spacing-1);
&:where(:nth-child(1 of .react-aria-ListBoxItem)) {
margin-top: var(--spacing);
}
&:where(:nth-last-child(1 of .react-aria-ListBoxItem)) {
margin-bottom: var(--spacing);
}
[role='presentation'] > & {
margin: 0; /* virtualizer */
}
&:not(:last-child)::after {
bottom: 0;
width: calc(100% - var(--spacing-6));
border-bottom: 0.5px solid var(--border-color);
}
&[data-selected] {
&:has(+ [data-selected]),
&:has(+ .react-aria-DropIndicator + [data-selected]) {
border-end-start-radius: 0;
border-end-end-radius: 0;
}
+ [data-selected],
+ .react-aria-DropIndicator + [data-selected] {
border-start-start-radius: 0;
border-start-end-radius: 0;
}
}
}
}
&[data-orientation='horizontal'] {
flex-direction: row;
width: 100%;
.react-aria-ListBoxItem {
min-width: max-content;
margin-block: var(--spacing-1);
&:nth-child(1 of .react-aria-ListBoxItem) {
margin-inline-start: var(--spacing);
}
&:nth-last-child(1 of .react-aria-ListBoxItem) {
margin-inline-end: var(--spacing);
}
&:not(:last-child)::after {
inset-inline-end: 0;
height: calc(100% - var(--spacing-2));
border-inline-end: 0.5px solid var(--border-color);
}
&[data-selected] {
&:has(+ [data-selected]),
&:has(+ .react-aria-DropIndicator + [data-selected]) {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
+ [data-selected],
+ .react-aria-DropIndicator + [data-selected] {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
}
}
}
}
&[data-layout='grid'] {
display: grid;
gap: var(--spacing-1);
width: fit-content;
max-width: 100%;
padding: var(--spacing-1);
&[data-orientation='vertical'] {
grid-template-columns: 1fr 1fr;
}
&[data-orientation='horizontal'] {
grid-auto-flow: column;
grid-template-rows: auto auto;
grid-template-columns: none;
grid-auto-columns: 160px;
}
}
}
.react-aria-ListBoxLoadingIndicator {
display: flex;
align-items: center;
justify-content: center;
height: 24px;
width: 100%;
}
.dropdown-listbox {
display: block;
max-height: inherit;
overflow: auto;
border-radius: inherit;
forced-color-adjust: none;
outline: none;
box-sizing: border-box;
font: var(--font-size) system-ui;
color: var(--text-color);
&[data-empty] {
min-height: var(--spacing-8);
padding: var(--spacing-2);
}
.react-aria-Header {
padding-left: var(--spacing-7);
}
}
.dropdown-item {
padding: calc((var(--spacing-8) - 1lh) / 2) var(--spacing-3) calc((var(--spacing-8) - 1lh) / 2)
var(--spacing-6);
box-sizing: border-box;
border-radius: var(--radius);
outline: none;
cursor: default;
color: var(--text-color);
font: var(--font-size) system-ui;
font-weight: 500;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
min-height: var(--spacing-8);
margin-inline: var(--spacing-1);
-webkit-tap-highlight-color: transparent;
&:first-of-type {
margin-top: var(--spacing);
}
&:last-of-type {
margin-bottom: var(--spacing);
}
&[data-selected] {
font-weight: 500;
color: var(--text-color);
.lucide-check {
position: absolute;
top: 50%;
left: var(--spacing-1);
translate: 0 -50%;
stroke-width: 3px;
width: var(--spacing-4);
height: var(--spacing-4);
}
}
&[data-pressed] {
background: var(--highlight-hover);
}
&[data-focused] {
background: var(--highlight-background);
color: var(--highlight-foreground);
}
[slot='label'] {
font-weight: 500;
}
[slot='description'] {
font-size: var(--font-size-sm);
}
&[data-disabled] {
color: var(--text-color-disabled);
}
&[href] {
text-decoration: none;
cursor: pointer;
}
}
components/ui/Popover.css
@import './theme.css';
.react-aria-Popover {
--background-color: var(--overlay-background);
outline: 1px solid var(--overlay-border);
box-sizing: border-box;
filter: drop-shadow(var(--popover-shadow));
border-radius: var(--radius-lg);
background: var(--background-color);
color: var(--text-color);
transition:
transform 200ms,
opacity 200ms;
font: var(--font-size) system-ui;
padding: 8px;
overflow: auto;
&[data-trigger='MenuTrigger'],
&[data-trigger='SubmenuTrigger'] {
padding: 0;
}
.react-aria-OverlayArrow svg {
display: block;
fill: var(--background-color);
stroke: var(--overlay-border);
paint-order: stroke;
stroke-width: 2px;
}
&[data-entering],
&[data-exiting] {
transform: var(--origin);
opacity: 0;
}
&[data-placement='top'] {
--origin: translateY(8px);
&:has(.react-aria-OverlayArrow) {
margin-bottom: 6px;
}
}
&[data-placement='bottom'] {
--origin: translateY(-8px);
&:has(.react-aria-OverlayArrow) {
margin-top: 6px;
}
.react-aria-OverlayArrow svg {
transform: rotate(180deg);
}
}
&[data-placement='right'] {
--origin: translateX(-8px);
&:has(.react-aria-OverlayArrow) {
margin-left: 6px;
}
.react-aria-OverlayArrow svg {
transform: rotate(90deg);
}
}
&[data-placement='left'] {
--origin: translateX(8px);
&:has(.react-aria-OverlayArrow) {
margin-right: 6px;
}
.react-aria-OverlayArrow svg {
transform: rotate(-90deg);
}
}
}