A powerfully simple React styling solution
Embellish offers a powerful and intuitive way to style your React application
deterministically, without runtime style injection or extra build steps. Easily
create a polymorphic component with your own custom style props, and use a pure
CSS mechanism to apply styles conditionally, creating hover effects, responsive
behavior, and more. Consider this example of a Box
component:
<Box
as="a"
href="https://github.com/embellishing/embellish"
color="#fff"
background="#03f"
hover:background="color-mix(in srgb, #03f, #fff 12.5%)"
active:background="#c30">
Get started
</Box>
With a styling API that builds upon the declarative nature of React, the Box
component enables you to manage complex styling scenarios with ease. Meanwhile,
Embellish's purely CSS-driven approach for defining conditions like hover
and
active
means that you can create dynamic and interactive UI elements without
compromising on performance or maintainability.
- Conditional styles with pure CSS: Under the hood, Embellish uses CSS Variables to apply styles conditionally based on pseudo-classes, at-rules, or arbitrary selector logic.
- First-class style props: Components expose CSS properties as first-class props. You can choose which ones to support or even define your own custom props.
- Style prop conditions: Specify the name of a condition as a style prop
modifier, e.g.
hover:background="#333"
, and its value will apply only under that condition. - Inline conditions: Conditions can be combined inline using logical operators, providing flexibility, promoting reuse, and keeping global CSS to a minimum.
- No runtime style injection: Avoid hydration mismatches, flashes of unstyled content, and questionable performance of runtime style injection.
- No build step: Simplify the development workflow by avoiding static analysis and extra build steps.
- Near-perfect code splitting: Most style information is embedded directly in component markup, with a minimal global style sheet used only to define reusable conditions.
- No cascade defects: Embellish's use of inline styles ensures that CSS rulesets can't "leak in" and modify private component implementation details.
Install @embellish/react using your package manager of choice, e.g.
npm install @embellish/react
Start by defining CSS hooks. These are all of the "selectors" you want to use throughout your app. These can be actual CSS selectors or even at-rules.
import { createHooks } from "@embellish/react";
const { StyleSheet, hooks } = createHooks([
"&:hover",
"&:focus",
"&:active",
"&:disabled",
"&[aria-disabled=true]",
"@media (width >= 600px)",
]);
Note
It's a good practice to keep these hooks as simple and generic as possible to promote reuse. Later, you can combine them to create more complex conditions.
The StyleSheet
component obtained in the previous step renders a small static
style sheet containing the CSS required to support conditional styling. Add this
to the root layout component or entry point of your app.
// e.g. src/main.tsx
root.render(
<StrictMode>
+ <StyleSheet />
<App />
</StrictMode>
);
A reusable condition assigns an alphanumeric alias (i.e. a valid prop name) to each hook. You can also define complex conditions using logical operators.
import { createConditions } from "@embellish/react";
const conditions = createConditions(hooks, {
hover: "&:hover",
focus: "&:focus",
active: "&:active",
disabled: { or: ["&:disabled", "&[aria-disabled=true]"] },
intent: { or: ["&:hover", "&:focus"] },
desktop: "@media (width >= 600px)",
});
Note
At this stage, it's still a good practice to consider the reusability of each complex condition defined here. You can define inline conditions later for one-off use cases.
Use the createStyleProps
function to define style props. The keys of the
configuration object are the prop names, with each entry consisting of either
- a function, parameterized by the prop value, which returns a React
CSSProperties
object; or true
, indicating that a default implementation should be used (standard CSS properties only)
import { createStyleProps } from "@embellish/react";
const styleProps = createStyleProps({
backgroundColor: (value: CSSProperties["backgroundColor"]) => ({
backgroundColor: value,
}), // defined as a function for illustrative purposes
border: true,
borderRadius: true,
color: true,
cursor: true,
display: true,
fontSize: true,
fontWeight: true,
outline: true,
outlineOffset: true,
padding: true,
transition: true,
});
Create a Box
component using the conditions defined in the previous step along
with your desired style props.
import { createComponent } from "@embellish/react";
const Box = createComponent({
displayName: "Box", // recommended for debugging purposes
defaultAs: "div", // optional, any HTML tag or component
defaultStyle: () => ({
// optional, a regular React style object consisting of "base" styles
boxSizing: "border-box",
textDecoration: "none",
}),
conditions,
styleProps,
});
Use your Box
component to create a styled button:
function CtaButton({
href,
children,
disabled,
}: {
href: string;
children?: ReactNode;
disabled?: boolean;
}) {
return (
<Box
as="a"
href={href}
aria-disabled={disabled}
display="inline-block"
backgroundColor="#6200ea"
color="#ffffff"
padding="12px 24px"
border="none"
borderRadius="4px"
cursor="pointer"
fontSize="16px"
fontWeight="bold"
transition="background-color 0.3s, color 0.3s"
intent:backgroundColor="#3700b3"
active:backgroundColor="#6200ea"
active:color="#bb86fc"
focus:outline="2px solid #03dac6"
focus:outlineOffset="2px"
disabled:cursor="not-allowed">
{children}
</Box>
);
}
You can compose conditions inline using logical operators, creating maximum
flexibility and reuse for the hooks you defined in Step 1 above. Simply pass
additional conditions to the conditions
prop, and then use them as style prop
modifiers:
function CtaButton({
href,
children,
disabled,
}: {
href: string;
children?: ReactNode;
disabled?: boolean;
}) {
return (
<Box
+ conditions={{
+ intentEnabled: {
+ and: ["intent", { not: "disabled" }],
+ },
+ activeEnabled: {
+ and: ["active", { not: "disabled" }],
+ },
+ focusEnabled: {
+ and: ["focus", { not: "disabled" }],
+ },
+ }}
as="a"
href={href}
aria-disabled={disabled}
display="inline-block"
backgroundColor="#6200ea"
color="#ffffff"
padding="12px 24px"
border="none"
borderRadius="4px"
cursor="pointer"
fontSize="16px"
fontWeight="bold"
transition="background-color 0.3s, color 0.3s"
- intent:backgroundColor="#3700b3"
- active:backgroundColor="#6200ea"
- active:color="#bb86fc"
- focusEnabled:outline="2px solid #03dac6"
+ intentEnabled:backgroundColor="#3700b3"
+ activeEnabled:backgroundColor="#6200ea"
+ activeEnabled:color="#bb86fc"
+ focusEnabled:outline="2px solid #03dac6"
focus:outlineOffset="2px"
disabled:cursor="not-allowed">
{children}
</Box>
);
}
Chrome |
Edge |
Safari |
Firefox |
Opera |
---|---|---|---|---|
49+ |
16+ |
10+ |
31+ |
36+ |
Contributions are welcome. Please see the contributing guidelines for more information.
Embellish is offered under the MIT license.