Skip to content

Instantly share code, notes, and snippets.

@bengry
Created August 31, 2024 20:35
Show Gist options
  • Save bengry/a11b9e5f19341ebdf42a42b68b728da4 to your computer and use it in GitHub Desktop.
Save bengry/a11b9e5f19341ebdf42a42b68b728da4 to your computer and use it in GitHub Desktop.
Utility function for class organization when using libraries like Tailwind, which get very verbose very fast
import { isPlainObject } from './isPlainObject';
import { StringWithAutocomplete } from './StringWithAutocomplete';
import { ClassValue as GenericClassValue, clsx } from 'clsx';
type ClassesValue = string;
/**
* A utility function to allow easier authoring of Tailwind-based component classes.
* Usually Tailwind-based components are basically just a never-ending string of random classes (example: https://flowbite.com/docs/components/buttons/).
*
* This function aims to bring some sense and order to that, by allowing you to categorize classes into different
* categories, structured as an object, where the keys are the groups (and don't have any technical meaning), and the
* values are the classes related to that group.
*
* @example
* ```ts
* const button = classes({
* reset: 'p-0 m-0', // classes related to CSS resets
* styles: 'bg-blue-500 text-white', // classes related to the style of the button
* layout: 'flex items-center justify-center', // classes related to the layout of the button
* hover: 'hover:bg-blue-600', // classes related to the hover state of the button
* active: 'active:bg-blue-700', // classes related to the active state of the button
* focusRing: 'focus:ring-2 focus:ring-blue-500', // classes related to the focus ring of the button
* })
*
* // console.log(button) // logs 'p-0 m-0 bg-blue-500 text-white flex items-center justify-center hover:bg-blue-600 active:bg-blue-700 focus:ring-2 focus:ring-blue-500'
* ````
*/
export function classes<const T extends CategorizedClassValue>(
...values: ReadonlyArray<Partial<T>>
): ClassesValue {
return clsx(
values.map(value => (isPlainObject(value) ? Object.values(value) : value))
);
}
type ClassDictionary = Record<
StringWithAutocomplete<ClassCategory>,
GenericClassValue
>;
type CategorizedClassValue =
| Exclude<GenericClassValue, Record<string, unknown>>
| ClassDictionary;
type ClassCategory =
| 'reset'
| 'variables'
| 'base'
| 'animation'
| 'disabled'
| 'focus'
| 'hover'
| 'active'
| 'focusRing'
| 'positioning'
| 'style'
| 'layout';
export function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false
const proto = Object.getPrototypeOf(obj);
return proto !== null && Object.getPrototypeOf(proto) === null;
}
/**
* A utility type around `string` that allows for autocomplete on the string on a specific set of values, yet accepting any string.
*
* @see https://twitter.com/diegohaz/status/1524257274012876801
*/
export type StringWithAutocomplete<S extends string> =
| S
| (string & Record<never, never>);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment