Skip to content

embellishing/embellish

Repository files navigation

Embellish Embellish
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.

Features

  • 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.

Installation

Install @embellish/react using your package manager of choice, e.g.

npm install @embellish/react

Getting started

Step 1: CSS hooks

Step 1a: Define hooks

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.

Step 1b: Add style sheet

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>
);

Step 2: Create reusable conditions

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.

Step 3: Create style props

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,
});

Step 4: Create a component

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,
});

Step 5: Use the component

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>
  );
}

Advanced usage

Inline conditions

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>
  );
}

Browser support

Chrome
Chrome
Edge
Edge
Safari
Safari
Firefox
Firefox
Opera
Opera
49+
16+
10+
31+
36+

Contributing

Contributions are welcome. Please see the contributing guidelines for more information.

License

Embellish is offered under the MIT license.