Skip to content

Instantly share code, notes, and snippets.

@praskoson
Created March 19, 2023 20:26
Show Gist options
  • Save praskoson/070a119a00ce11fd922f817e4ef93f50 to your computer and use it in GitHub Desktop.
Save praskoson/070a119a00ce11fd922f817e4ef93f50 to your computer and use it in GitHub Desktop.
Reusable Select Component using Headless-UI & Tailwind
import {
Transition,
Listbox,
type ListboxProps,
type ListboxOptionsProps,
type ListboxOptionProps,
} from "@headlessui/react";
import clsx from "clsx";
import React from "react";
import { twMerge } from "tailwind-merge";
export const getValue = <T extends unknown[], R>(
valueOrFn: R | ((...args: T) => R),
...args: T
): R => {
if (typeof valueOrFn === "function") {
return (valueOrFn as (...args: T) => R)(...args);
}
return valueOrFn;
};
type SelectProps<TType> = ListboxProps<"div", TType, TType>;
export const CustomSelect = <TType,>(props: SelectProps<TType>) => {
const { children, className, ...rest } = props;
return (
<Listbox<"div", TType, TType>
as="div"
className={(bag) => twMerge("relative", getValue(className, bag))}
{...rest}
>
{typeof children === "function" ? (bag) => children(bag) : children}
</Listbox>
);
};
type SelectOptionsProps = Omit<ListboxOptionsProps<"ul">, "unmount" | "as">;
const CustomSelectOptions = (props: SelectOptionsProps, ref: React.Ref<HTMLUListElement>) => {
// Something is broken when using className as a function with render props,
// It's because of the Transition wrapper, but I don't know how to "pass" the correct render prop.
const { className, children, ...rest } = props;
return (
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options
as="ul"
ref={ref}
{...rest}
className={twMerge(
clsx(
"absolute left-1/2 z-20 mt-3 w-full origin-top-left -translate-x-1/2 space-y-3 rounded-2xl bg-neutral-600",
"p-2 shadow-xl ring-1 ring-neutral-500 ring-opacity-100 focus:outline-none"
),
typeof className === "string" ? className : ""
)}
>
{typeof children === "function" ? (bag) => children(bag) : children}
</Listbox.Options>
</Transition>
);
};
type SelectOptionProps<TType> = Omit<ListboxOptionProps<"li", TType>, "as">;
const CustomSelectOption = <TType,>(
props: SelectOptionProps<TType>,
ref: React.Ref<HTMLLIElement>
) => {
const { children, ...rest } = props;
return (
<Listbox.Option ref={ref} as="li" {...rest}>
{typeof children === "function" ? (bag) => children(bag) : children}
</Listbox.Option>
);
};
const SelectButton = Listbox.Button;
const SelectOptions = React.forwardRef(CustomSelectOptions);
const SelectOption = React.forwardRef(CustomSelectOption);
export const Select = Object.assign(CustomSelect, {
Button: SelectButton,
Options: SelectOptions,
Option: SelectOption,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment