react-class-composer
v0.4.7
Published
Simple tool to compose css classnames based on component props
Downloads
14
Readme
Simple tool to compose css classnames based on component props
Install
$ npm install react-class-composer
$ yarn add react-class-composer
Motivation
react-class-composer was built as a tool for creating low level basic building block components for new design systems or UI libraries that use utility-css frameworks to style components (like tailwind).
there are definitely other libraries that achieve this, and if you are looking to solve that problem and react-class-composer does not fit your needs, I encourage you to check them out: useFancy, use-utility-classes, React With Class
How does it work?
use createComponent()
we can create and forward a native HTML component:
import { createComponent } from "react-class-composer";
type BoxProps = {
display?: "flex" | "block" | "inline";
};
export const Box = createComponent<BoxProps>("div", {
base: "box-base",
options: {
display: {
flex: "display-flex",
block: "display-block",
inline: "display-inline",
},
},
});
Using the component:
<Box display='flex'><Box>
<Box display='inline'><Box>
<Box display='block'><Box>
HTML Output:
<div className="box-base display-flex"></div>
<div className="box-base display-inline"></div>
<div className="box-base display-block"></div>
Hooks
useClassComposer()
Hook
this hook acts like the createComponent
function, but lets you deal with all the component outer shell.
it returns a classname based on a config file, and requires a config
and props
object.
import React from "react";
import { useClassComposer } from "react-class-composer";
interface Props {
size: "small" | "medium" | "large";
something: React.ReactNode;
}
export const YourComponent: React.FC<Props> = (props) => {
const { className } = useClassComposer<Props>(
{
base: "base-class",
options: {
size: {
small: "small-class",
medium: "medium-class",
large: "large-class",
},
},
},
props
);
return (
<div className={className}>
your component
{props.something}
</div>
);
};
useClassname()
Hook
this hooks just compiles the @ClassDefinition
object into memoized string of classnames. its kind of like clsx()
. it takes a config
object and optionally a React.DependencyList
array.
import React from "react";
import { useClassname } from "react-class-composer";
const ComponentWithClass: React.FC = ({ props }) => {
const className = useClassname(
[
"btn",
"btn-something",
{ hover: "btn-hover" },
() => (props.something ? "btn-something" : "btn-not"),
],
props
);
return <button className={className}>click me!</button>;
};
the @ClassDefinition
type
export type ClassDefinition = string
| ClassDefinition[];
| ((value) => ClassDefinition)
| { [key: string]: ClassDefinition }
anytime you can define a class, you can use any combination of the following values:
String
options: {
prop: {
a: "simple",
b: "multiple classes in the same string" // will be .split(" ") before parsing
}
}
Array
any array of @ClassDefinition
values will be flattened and parsed.
options: {
prop: {
a: ["simple", "array"],
b: ["multi", ["level", "array"]],
c: [() => "string", {obj: "string"}]
}
}
Functions
you can use functions to generate dynamic classnames. functions can return any valid @ClassDefinition
excluding function
options: {
prop: {
a: () => "some-class-name"
},
anotherProp: (value) => `prop${value}`
}
Objects
any object beyond the first level (used to parse prop values) will be exploded into prefixed classes like key:value
. all keys need to be string, but the value can be any @ClassDefinition
options: {
prop: {
a: "a-value", // will apply `a-value` if <... prop="a" />
b: "b-value" // will apply `b-value` if <... prop="b" />
c: {hover: 'color-red'}// will apply `hover:color-red` if <... prop="c" />
},
}
Mixers
[TODO]
$
and $$
Prefixes
[TODO]
Full Example
(view Button.tsx) (view Tests)
import {
createComponent,
mixAddClass,
mixFunction,
mixRemoveClass,
} from "react-class-composer";
type ButtonProps = {
size: "tiny" | "small" | "medium" | "large";
variant?: "none" | "outline" | "filled";
rounded?: boolean;
anotherOption?: "on" | "off";
dynamicOptions?: number;
} & Partial<{
// alias:
round: ButtonProps["rounded"];
v: ButtonProps["variant"];
}>;
export const Button = createComponent<ButtonProps, "button">("button", {
"button",
{
/**
* Base: base classes, will always be applied
*/
base: [
"btn",
{ hover: ["btn-hover", "text-bold"] }, // {key: 'value'} pairs will be exploded into prefixed classes like `key:value`
() => "btn-base", // you can use functions to return a string or @ClassDefinition object
],
/**
* Mix: mix functions allow for conditional class toggling based on the multiple props.
*/
mix: [
// mixAddClass will add classes if all the conditions are true
mixAddClass(["size.tiny", "variant.outline"], "size-tiny-outline-mix"),
// mixRemoveClass will remove a class if all the conditions are true
mixRemoveClass(
["size.tiny", "variant.filled"],
["btn-base", { hover: "btn-hover" }, "hover:text-bold"]
),
// mix functions support wild card checks:
mixRemoveClass(["data-something.a"], ["btn-base"]),
// you can run your own mix functions
mixFunction(["anotherOption.*", "disabled.true"], (css) =>
css.add("any-anotherOptions-disabled-true")
),
// or simply just pass in a mix object.
{ when: ["type.reset"], run: (css) => css.add("btn-reset") },
// you can match any prop, even if its a native HTML element one
{
when: ["formNoValidate.true"],
run: (css) => css.add("form-no-validate"),
},
],
/**
* Alias: prop shortcuts for other options
*/
alias: {
// v="outline" is interpreted as variant="outline"
v: "variant",
round: "rounded",
},
/**
* Options: options are [key,value] pairs, where the key is the prop name
*/
options: {
size: {
// you can use string
tiny: "padding-tiny margin-tiny",
// or any combination of string[]
small: [
"padding-medium",
"margin-medium",
["text-medium", "font-something"],
],
// also supports () => string
medium: () => `medium-stuff class-returned-by-function`,
// any object key will be parsed as "prefixed" class name
large: {
large: ["text", "font", "padding"],
key: { abc: ["a", "b", "c"], num: ["n1", "n2", "n3"] },
},
},
// use $ as a prefix to mark a prop as "native" (comes from the native HTML element we are extending)
$type: {
submit: "btn-submit",
},
variant: {
none: "",
outline: "bg-white-500 text-black border border-gray-400",
filled: "bg-teal-200 text-white",
},
rounded: "rounded-2xl",
anotherOption: {
on: "option-on",
off: "options-off",
},
// dynamic options via function
dynamicOptions: (value) => {
if (value < 50) return "less-than-50";
if (value > 50) return "more-than-50";
return ["value-is-50", "dynamic-options-50"];
},
// use $ as a prefix to mark a prop as "native" (comes from the native HTML element we are extending)
$disabled: "btn-disabled",
// use $$ as a prefix to apply classes if a prop is present, ignoring what value it has
$$title: "btn-has-title",
// we can also target data-attributes:
"data-something": {
a: "something-a",
b: "something-b",
},
},
},
{
// defaults, will apply classes as if <... variant="none">
variant: "none",
}
);