A react tool to separate class name logic, create variants and manage styles.
npm install @classmatejs/reactA tool for managing React component class names, variants and styles.
``jsx
const SomeButton = ({ isLoading, ...props }) => {
const activeClass = isLoading
? "bg-blue-400 text-white"
: "bg-blue-800 text-blue-200";
return (
{...props}
className={transition-all mt-5 border-1 md:text-lg text-normal ${someConfig.transitionDurationEaseClass} ${activeClass} ${
props.className || ""
}}`
>
{props.children}
);
};
`js
const SomeButton = cm.button
text-normal
md:text-lg
mt-5
border-1
transition-all
${someConfig.transitionDurationEaseClass}
${({ $isLoading }) => $isLoading && "opacity-90 pointer-events-none"};`
- Class name-focused components
- Variants
- Extend components
- Dynamic styles
- TypeScript support
- Tested with SSR Frameworks
- Classname merging
- Features
- Getting started
- Basic usage
- Usage with props
- Create Variants
- Extend components
- Add CSS Styles
- Use inside React components
- Add logic headers
- Recipes for cm.extend
- Use cm for creating base component
- Auto infer types for props
- Extending other lib components / any as Input
Make sure you have installed React (> 16.8.0) in your
project.
`bash`
npm i @classmatejs/reactor
yarn add @classmatejs/react
Create a component by calling cm with a tag name and a template literal
string.
`tsx
import cm from "@classmatejs/react";
const Container = cm.div
py-2
px-5
min-h-24;`
// transforms to:
Pass props to the component and use them in the template literal string and in
the component prop validation.
`tsx
// hey typescript
interface ButtonProps {
$isActive?: boolean;
$isLoading?: boolean;
}
const SomeButton = cm.button
text-lg
mt-5
${({ $isActive }) => ($isActive ? "bg-blue-400 text-white" : "bg-blue-400 text-blue-200")}
${({ $isLoading }) => $isLoading && "opacity-90 pointer-events-none"};`
// transforms to
we prefix the props incoming to dc with a $ sign. This is a important
convention to distinguish dynamic props from the ones we pass to the component.
_This pattern should also avoid conflicts with reserved prop names._
Create variants by passing an object to the variants key like in
cva. The key should match the
prop name and the value should be a function that returns a string. You could
also re-use the props in the function.
`tsx
interface AlertProps {
$severity: "info" | "warning" | "error";
$isActive?: boolean;
}
const Alert = cm.div.variants
// optional
base: (p) =>
${p.isActive ? "custom-active" : "custom-inactive"}
p-4
rounded-md
,bg-blue-100 text-blue-800 ${p.$isActive ? "shadow-lg" : ""}
// required
variants: {
$severity: {
warning: "bg-yellow-100 text-yellow-800",
info: (p) =>
,bg-red-100 text-red-800 ${p.$isActive ? "ring ring-red-500" : ""}
error: (p) =>
,
},
},
// optional - used if no variant was found
defaultVariant: {
$severity: "info",
},
});
export default () =>
// outputs:
$3
As seen above, we also pass
AlertProps to the variants, which can cause loose
types. If you want to separate the base props from the variants, you can pass a
second type to the variants function so that only those props are available in
the variants.`tsx
interface AlertProps {
$isActive?: boolean;
}
interface AlertVariants {
$severity: "info" | "warning" | "error";
}
const Alert = cm.div.variants({
base: p-4 rounded-md,
variants: {
// in here there are only the keys from AlertVariants available
$severity: {
// you can use the props from AlertProps here again
warning: "bg-yellow-100 text-yellow-800",
info: (p) =>
bg-blue-100 text-blue-800 ${p.$isActive ? "shadow-lg" : ""},
error: (p) =>
bg-red-100 text-red-800 ${p.$isActive ? "ring ring-red-500" : ""},
},
},
// optional - used if no variant was found
defaultVariant: {
$severity: "info",
},
});
`Extend
Extend a component directly by passing the component and the tag name.
`tsx
import MyOtherComponent from "./MyOtherComponent"; // () =>
import cm from "@classmatejs/react";const Container = cm.extend(MyOtherComponent)
;
// transforms to:
`Add CSS Styles
You can use CSS styles in the template literal string with the
style function.
This function takes an object with CSS properties and returns a string. We can
use the props from before.`tsx
// Base:
const StyledButton = cm.button<{ $isDisabled: boolean }>;
export default () => ;
// outputs:
``tsx
// Extended:
const BaseButton = cm.button<{ $isActive?: boolean }>;
const ExtendedButton = cm.extend(BaseButton)<{ $isLoading?: boolean }>;
export default () => ;
// outputs:
`$3
When you need to create a classmate component inside another React component
(for example, when the configuration depends on runtime-only values), wrap the
factory with
useClassmate. This memoizes the result and avoids creating a
brand-new component on every render.`tsx
import cm, { useClassmate } from "@classmatejs/react";const WorkoutDay = ({ status }: { status: "completed" | "pending" }) => {
const StyledDay = useClassmate(
() =>
cm.div.variants({
base: "rounded border p-4 text-sm",
variants: {
$status: {
completed: "border-green-400 bg-green-50",
pending: "border-yellow-400 bg-yellow-50",
},
},
}),
[status], // recompute when dependencies change
);
return Workout details ;
};
`> The dependency array behaves like
React.useMemo. Pass everything the factory
> closes over if you expect the component to update when those values change.$3
Use
.logic() to run arbitrary JavaScript once per render before your class
names or variants are computed. The return value is shallow-merged back into the
props, so you can derive $ props, DOM attributes, or anything else your
component needs without additional hooks.`tsx
type DayStatus = "completed" | "pending"interface WorkoutProps {
workouts: unknown[]
allResolved: boolean
hasCompleted: boolean
hasSkipped: boolean
$status?: DayStatus
}
const WorkoutDay = cm.div
.logic((props) => {
const status = deriveDayStatus(props)
return {
$status: status,
["data-status"]: status,
}
})
.variants({
base: "rounded border p-4",
variants: {
$status: {
completed: "bg-green-50 border-green-400",
pending: "bg-white border-slate-200",
},
},
})
// Consumers only pass raw workout data ā the logic header derives $status for you.
`> Return values from
.logic() are merged in order, so later logic calls can
> reference earlier results or override them.Recipes for
cm.extendWith
cm.extend, you can build upon any base React component, adding new styles
and even supporting additional props. This makes it easy to create reusable
component variations without duplicating logic.`tsx
import { ArrowBigDown } from "lucide-react";
import cm from "@classmatejs/react";const StyledLucideArrow = cm.extend(ArrowBigDown)
;// ts: we can pass only props which are accessible on a
lucid-react Component
export default () => ;
`ā ļø Having problems by extending third party components, see:
Extending other lib components
Now we can define a base component, extend it with additional styles and
classes, and pass properties. You can pass the types to the
extend function to
get autocompletion and type checking.`tsx
import cm from "@classmatejs/react";interface StyledSliderItemBaseProps {
$active: boolean;
}
const StyledSliderItemBase = cm.button
;interface NewStyledSliderItemProps extends StyledSliderItemBaseProps {
$secondBool: boolean;
}
const NewStyledSliderItemWithNewProps = cm.extend(
StyledSliderItemBase,
)
;export default () => (
);
// outputs:
`$3
`tsx
interface ButtonProps extends InputHTMLAttributes {
$severity: "info" | "warning" | "error";
$isActive?: boolean;
}const Alert = cm.input.variants({
base: "p-4",
variants: {
$severity: {
info: (p) =>
bg-blue-100 text-blue-800 ${p.$isActive ? "shadow-lg" : ""},
},
},
});const ExtendedButton = cm.extend(Alert)<{ $test: boolean }>
;export default () => ;
// outputs:
`$3
By passing the component, we can validate the component to accept tag related
props. This is useful if you wanna rely on the props for a specific element
without the
$ prefix.`tsx
// if you pass rc component it's types are validated
const ExtendedButton = cm.extend(cm.button);// infers the type of the input element + add new props
const MyInput = ({ ...props }: HTMLAttributes) => (
);
const StyledDiv = cm.extend(MyInput)<{ $trigger?: boolean }>
;
`$3
Unfortunately we cannot infer the type directly of the component if it's
any
or loosely typed. But we can use a intermediate step to pass the type to the
extend function.`tsx
import { ComponentProps } from 'react'
import { MapContainer } from 'react-leaflet'
import { Field, FieldConfig } from 'formik'
import cm, { CmBaseComponent } from 'react-classmate'// we need to cast the type to ComponentProps
type StyledMapContainerType = ComponentProps
const StyledMapContainer: CmBaseComponent = cm.extend(MapContainer)
export const Component = () =>
// or with Formik
import { Field, FieldConfig } from 'formik'
type FieldComponentProps = ComponentProps<'input'> & FieldConfig
const FieldComponent = ({ ...props }: FieldComponentProps) =>
const StyledField = cm.extend(FieldComponent)<{ $error: boolean }>
export const Component = () =>
`CommonJS
If you are using CommonJS, you can import the library like this:
`js
const cm = require("@classmatejs/react").default;// or
const { default: cm } = require("@classmatejs/react");
`Tailwind Merge
React-classmate uses tailwind-merge
under the hood to merge class names. The last class name will always win, so you
can use it to override classes.
Upcoming
- bug / troubleshoot: classnames set by ref.current (useRef) will be overwritten
as soon component rerenders
- needs at least a small article in the docs
-
cm.raw() and cm.raw.variants() for only using rc syntax for classnames
(output as string)
- Variants for cm.extend
- named lib import for CommonJS (currently only .default`) -- Means we need to