Theming
Style config API
Create custom components with multiple parts, multi-variant styles, and theme customization support.
Warning: This is an advanced API used internally by Hope UI components. In most cases, you don't need it in your application.
Introduction
The style config API allows you to create components with support for:
- multiple parts (ex: an Alert component with a root, title, and description parts)
- multi-variant styles (like described in the Styled components section)
- customization at the theme level (as described in the Customize theme section)
- polymorphic
as
prop, style props, andsx
The below example shows how to create a custom Alert
component with style config API support.
Creating a style config
Defining component parts
First, we define which parts compose our component.
tsx
// alert.styles.tsexport type AlertParts = "root" | "title" | "description";
tsx
// alert.styles.tsexport type AlertParts = "root" | "title" | "description";
Defining variants
Then, we define which variants our component will accept. Each variant will become a prop of the component.
tsx
// alert.styles.tsexport type AlertParts = "root" | "title" | "description";interface AlertVariants {variant: "solid" | "soft";status: "success" | "info" | "warning" | "danger";}
tsx
// alert.styles.tsexport type AlertParts = "root" | "title" | "description";interface AlertVariants {variant: "solid" | "soft";status: "success" | "info" | "warning" | "danger";}
Creating the styles
Next, we create our component's style config.
The createStyleConfig
function takes an object as the first parameter, in which each key refers to a part of the component, and each value defines the baseStyle
, variants
and compoundVariants
styles of that part.
tsx
// alert.styles.tsimport { createStyleConfig } from "@hope-ui/core";export type AlertParts = "root" | "title" | "description";interface AlertVariants {variant: "solid" | "soft";status: "success" | "info" | "warning" | "danger";}export const useAlertStyleConfig = createStyleConfig<AlertParts, AlertVariants>({root: {baseStyle: {// base style of the "root" part},variants: {variant: {solid: {// style of the "root" part when the `variant="solid"` prop is passed},soft: {},},status: {success: {// style of the "root" part when the `status="success"` prop is passed},info: {},warning: {},danger: {},},},compoundVariants: [{variants: {variant: "solid",status: "success",},style: {// style of the "root" part when both `variant="solid"` and `status="success"` props are passed},},],},title: {// style config of the "title" part, if any},description: {// style config of the "description" part, if any},});
tsx
// alert.styles.tsimport { createStyleConfig } from "@hope-ui/core";export type AlertParts = "root" | "title" | "description";interface AlertVariants {variant: "solid" | "soft";status: "success" | "info" | "warning" | "danger";}export const useAlertStyleConfig = createStyleConfig<AlertParts, AlertVariants>({root: {baseStyle: {// base style of the "root" part},variants: {variant: {solid: {// style of the "root" part when the `variant="solid"` prop is passed},soft: {},},status: {success: {// style of the "root" part when the `status="success"` prop is passed},info: {},warning: {},danger: {},},},compoundVariants: [{variants: {variant: "solid",status: "success",},style: {// style of the "root" part when both `variant="solid"` and `status="success"` props are passed},},],},title: {// style config of the "title" part, if any},description: {// style config of the "description" part, if any},});
Even if some parts of your component don't require styles, you have to define them with an empty object.
Adding default variants
You can set default variants to be used in the style config by passing a second parameter to createStyleConfig
:
tsx
// alert.styles.tsimport { createStyleConfig } from "@hope-ui/core";export type AlertParts = "root" | "title" | "description";interface AlertVariants {variant: "solid" | "soft";status: "success" | "info" | "warning" | "danger";}export const useAlertStyleConfig = createStyleConfig<AlertParts, AlertVariants>({// ...style config definition},{variant: "solid",status: "info",});
tsx
// alert.styles.tsimport { createStyleConfig } from "@hope-ui/core";export type AlertParts = "root" | "title" | "description";interface AlertVariants {variant: "solid" | "soft";status: "success" | "info" | "warning" | "danger";}export const useAlertStyleConfig = createStyleConfig<AlertParts, AlertVariants>({// ...style config definition},{variant: "solid",status: "info",});
Creating props type
Using the StyleConfigProps
utility type, we can generate a props type from the style config.
tsx
// alert.styles.tsimport { createStyleConfig, StyleConfigProps } from "@hope-ui/core";export type AlertParts = "root" | "title" | "description";interface AlertVariants {variant: "solid" | "soft";status: "success" | "info" | "warning" | "danger";}export const useAlertStyleConfig = createStyleConfig<AlertParts, AlertVariants>({// ...style config definition});export type AlertStyleConfigProps = StyleConfigProps<typeof useAlertStyleConfig>;
tsx
// alert.styles.tsimport { createStyleConfig, StyleConfigProps } from "@hope-ui/core";export type AlertParts = "root" | "title" | "description";interface AlertVariants {variant: "solid" | "soft";status: "success" | "info" | "warning" | "danger";}export const useAlertStyleConfig = createStyleConfig<AlertParts, AlertVariants>({// ...style config definition});export type AlertStyleConfigProps = StyleConfigProps<typeof useAlertStyleConfig>;
This type is made up of the AlertVariants
type defined above as Partial and two more props:
styleConfigOverride
: used to override the base style config created withcreateStyleConfig
unstyled
: Whether the base style config created withcreateStyleConfig
should be applied
Integrate with the components
Now, let's create the Alert
components that will consume our style config.
Creating the components
We create our components with the createHopeComponent
function, which adds support for the polymorphic as
prop, style props, and sx
.
tsx
// alert.tsximport { createHopeComponent } from "@hope-ui/core";import { AlertStyleConfigProps } from "./alert.styles.ts";export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {// TODO});export const AlertTitle = createHopeComponent<"div">(props => {// TODO});export const AlertDescription = createHopeComponent<"div">(props => {// TODO});
tsx
// alert.tsximport { createHopeComponent } from "@hope-ui/core";import { AlertStyleConfigProps } from "./alert.styles.ts";export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {// TODO});export const AlertTitle = createHopeComponent<"div">(props => {// TODO});export const AlertDescription = createHopeComponent<"div">(props => {// TODO});
Consuming the style config
Let's start by consuming our style config in the root Alert
component.
The useAlertStyleConfig
we've created takes two parameters:
- A name of your choice, used for theme level customization (described below)
- The
styleConfigOverride
,unstyled
and other variant props
It returns two accessors:
baseClasses
: the base classes to apply to each part of the componentstyleOverrides
: the style overrides for each part of the component
tsx
// alert.tsximport { Box, createHopeComponent } from "@hope-ui/core";import { clsx } from "clsx";import { splitProps } from "solid-js";import { AlertStyleConfigProps, useAlertStyleConfig } from "./alert.styles.ts";export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {const [local, styleConfigProps, others] = splitProps(props,["class", "children"],["styleConfigOverride", "unstyled", "variant", "status"]);const { baseClasses, styleOverrides } = useAlertStyleConfig("Alert", styleConfigProps);return (<Box class={clsx(baseClasses().root, local.class)} __css={styleOverrides().root} {...others}>{local.children}</Box>);});// ...AlertTitle and AlertDescription components
tsx
// alert.tsximport { Box, createHopeComponent } from "@hope-ui/core";import { clsx } from "clsx";import { splitProps } from "solid-js";import { AlertStyleConfigProps, useAlertStyleConfig } from "./alert.styles.ts";export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {const [local, styleConfigProps, others] = splitProps(props,["class", "children"],["styleConfigOverride", "unstyled", "variant", "status"]);const { baseClasses, styleOverrides } = useAlertStyleConfig("Alert", styleConfigProps);return (<Box class={clsx(baseClasses().root, local.class)} __css={styleOverrides().root} {...others}>{local.children}</Box>);});// ...AlertTitle and AlertDescription components
Note that styleOverrides
are passed to the __css
prop. It's like the sx
prop but has lower
specificity. This allows us to override the component styles with style props and sx
later.
Providing style config to descendants
We need a way to access the baseClasses
and styleOverrides
in AlertTitle
and AlertDescription
components. The easiest way to do that is to use a context.
tsx
// alert.tsximport { Box, createHopeComponent, SystemStyleObject } from "@hope-ui/core";import { clsx } from "clsx";import { Accessor, createContext, useContext, splitProps } from "solid-js";import { AlertParts, AlertStyleConfigProps, useAlertStyleConfig } from "./alert.styles.ts";export interface AlertContextValue {baseClasses: Accessor<Record<AlertParts, string>>;styleOverrides: Accessor<Record<AlertParts, SystemStyleObject>>;}export const AlertContext = createContext<AlertContextValue>();export function useAlertContext() {return useContext(AlertContext);}export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {const [local, styleConfigProps, others] = splitProps(props,["class", "children"],["styleConfigOverride", "unstyled", "variant", "status"]);const { baseClasses, styleOverrides } = useAlertStyleConfig("Alert", styleConfigProps);return (<AlertContext.Provider value={{ baseClasses, styleOverrides }}><Box class={clsx(baseClasses().root, local.class)} __css={styleOverrides().root} {...others}>{local.children}</Box></AlertContext.Provider>);});// ...AlertTitle and AlertDescription components
tsx
// alert.tsximport { Box, createHopeComponent, SystemStyleObject } from "@hope-ui/core";import { clsx } from "clsx";import { Accessor, createContext, useContext, splitProps } from "solid-js";import { AlertParts, AlertStyleConfigProps, useAlertStyleConfig } from "./alert.styles.ts";export interface AlertContextValue {baseClasses: Accessor<Record<AlertParts, string>>;styleOverrides: Accessor<Record<AlertParts, SystemStyleObject>>;}export const AlertContext = createContext<AlertContextValue>();export function useAlertContext() {return useContext(AlertContext);}export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {const [local, styleConfigProps, others] = splitProps(props,["class", "children"],["styleConfigOverride", "unstyled", "variant", "status"]);const { baseClasses, styleOverrides } = useAlertStyleConfig("Alert", styleConfigProps);return (<AlertContext.Provider value={{ baseClasses, styleOverrides }}><Box class={clsx(baseClasses().root, local.class)} __css={styleOverrides().root} {...others}>{local.children}</Box></AlertContext.Provider>);});// ...AlertTitle and AlertDescription components
Then, in AlertTitle
and AlertDescription
, we use useAlertContext
to consume that context.
tsx
// alert.tsx// ...imports, context and Alert componentexport const AlertTitle = createHopeComponent<"div">(props => {const [local, others] = splitProps(props, ["class"]);const { baseClasses, styleOverrides } = useAlertContext();return (<Boxclass={clsx(baseClasses().title, local.class)}__css={styleOverrides().title}{...others}/>);});export const AlertDescription = createHopeComponent<"div">(props => {const [local, others] = splitProps(props, ["class"]);const { baseClasses, styleOverrides } = useAlertContext();return (<Boxclass={clsx(baseClasses().description, local.class)}__css={styleOverrides().description}{...others}/>);});
tsx
// alert.tsx// ...imports, context and Alert componentexport const AlertTitle = createHopeComponent<"div">(props => {const [local, others] = splitProps(props, ["class"]);const { baseClasses, styleOverrides } = useAlertContext();return (<Boxclass={clsx(baseClasses().title, local.class)}__css={styleOverrides().title}{...others}/>);});export const AlertDescription = createHopeComponent<"div">(props => {const [local, others] = splitProps(props, ["class"]);const { baseClasses, styleOverrides } = useAlertContext();return (<Boxclass={clsx(baseClasses().description, local.class)}__css={styleOverrides().description}{...others}/>);});
Creating a component theme type
We need to create a component theme type to get TypeScript support in the theme object. To do that, Hope UI exposes the ComponentTheme
utility type.
In the example below, only variant
and status
from the AlertStyleConfigProps
type will be available to set in the theme default props section.
tsx
// alert.tsximport { Box, ComponentTheme, createHopeComponent } from "@hope-ui/core";import { clsx } from "clsx";import { splitProps } from "solid-js";import { AlertParts, AlertStyleConfigProps, useAlertStyleConfig } from "./alert.styles.ts";export type AlertTheme = ComponentTheme<AlertStyleConfigProps, "variant" | "status">;export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {const [local, styleConfigProps, others] = splitProps(props,["class", "children"],["styleConfigOverride", "unstyled", "variant", "status"]);const { baseClasses, styleOverrides } = useAlertStyleConfig("Alert", styleConfigProps);return (<AlertContext.Provider value={{ baseClasses, styleOverrides }}><Box class={clsx(baseClasses().root, local.class)} __css={styleOverrides().root} {...others}>{local.children}</Box></AlertContext.Provider>);});// ...other components
tsx
// alert.tsximport { Box, ComponentTheme, createHopeComponent } from "@hope-ui/core";import { clsx } from "clsx";import { splitProps } from "solid-js";import { AlertParts, AlertStyleConfigProps, useAlertStyleConfig } from "./alert.styles.ts";export type AlertTheme = ComponentTheme<AlertStyleConfigProps, "variant" | "status">;export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {const [local, styleConfigProps, others] = splitProps(props,["class", "children"],["styleConfigOverride", "unstyled", "variant", "status"]);const { baseClasses, styleOverrides } = useAlertStyleConfig("Alert", styleConfigProps);return (<AlertContext.Provider value={{ baseClasses, styleOverrides }}><Box class={clsx(baseClasses().root, local.class)} __css={styleOverrides().root} {...others}>{local.children}</Box></AlertContext.Provider>);});// ...other components
Add theme default props support
We also need to allow default props from the theme to be merged in the component.
To do that, we use the mergeThemeProps
function and pass it the following parameters:
- The name used to retrieve the component in the theme (should be the same used in
useAlertStyleConfig
) - The fallback default props (if the theme does not provide them)
- The component props object
tsx
// alert.tsximport { Box, ComponentTheme, createHopeComponent, mergeThemeProps } from "@hope-ui/core";import { clsx } from "clsx";import { splitProps } from "solid-js";import { AlertParts, AlertStyleConfigProps, useAlertStyleConfig } from "./alert.styles.ts";export type AlertTheme = ComponentTheme<AlertStyleConfigProps, "variant" | "status">;export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {props = mergeThemeProps("Alert", { status: "info" }, props);const [local, styleConfigProps, others] = splitProps(props,["class", "children"],["styleConfigOverride", "unstyled", "variant", "status"]);const { baseClasses, styleOverrides } = useAlertStyleConfig("Alert", styleConfigProps);return (<AlertContext.Provider value={{ baseClasses, styleOverrides }}><Box class={clsx(baseClasses().root, local.class)} __css={styleOverrides().root} {...others}>{local.children}</Box></AlertContext.Provider>);});// ...other components
tsx
// alert.tsximport { Box, ComponentTheme, createHopeComponent, mergeThemeProps } from "@hope-ui/core";import { clsx } from "clsx";import { splitProps } from "solid-js";import { AlertParts, AlertStyleConfigProps, useAlertStyleConfig } from "./alert.styles.ts";export type AlertTheme = ComponentTheme<AlertStyleConfigProps, "variant" | "status">;export const Alert = createHopeComponent<"div", AlertStyleConfigProps>(props => {props = mergeThemeProps("Alert", { status: "info" }, props);const [local, styleConfigProps, others] = splitProps(props,["class", "children"],["styleConfigOverride", "unstyled", "variant", "status"]);const { baseClasses, styleOverrides } = useAlertStyleConfig("Alert", styleConfigProps);return (<AlertContext.Provider value={{ baseClasses, styleOverrides }}><Box class={clsx(baseClasses().root, local.class)} __css={styleOverrides().root} {...others}>{local.children}</Box></AlertContext.Provider>);});// ...other components
The name provided to mergeThemeProps
should be the same used in useAlertStyleConfig
.
Using the component
Finally, our Alert
component can be used like this:
tsx
<Alert variant="solid" status="info"><AlertTitle>Get ready!</AlertTitle><AlertDescription>Hope UI 1.0 is going live soon.</AlertDescription></Alert>
tsx
<Alert variant="solid" status="info"><AlertTitle>Get ready!</AlertTitle><AlertDescription>Hope UI 1.0 is going live soon.</AlertDescription></Alert>
It also supports the polymorphic as
prop, style props, and sx
.
tsx
<Alert variant="solid" status="info" as="section" p={4}><AlertTitle as="h2" fontWeight="semibold" mb={2}>Get ready!</AlertTitle><AlertDescription as="span">Hope UI 1.0 is going live soon.</AlertDescription></Alert>
tsx
<Alert variant="solid" status="info" as="section" p={4}><AlertTitle as="h2" fontWeight="semibold" mb={2}>Get ready!</AlertTitle><AlertDescription as="span">Hope UI 1.0 is going live soon.</AlertDescription></Alert>