Skip to main content

Composing class names

A class name targeting styles of the sub-components created by composition.

Defining nested className value

To define a nested class names for the sub-components use a nested object.

Object properties (keys) identifiers must start with lowercase or uppercase ASCII letter.

The only exception being the HOST_KEY.

import { defineClassName } from "@site/../packages/class-name/dist";

const subComponentsClassName = defineClassName({
// 2.1: Stateless:
header: 'hello world',
// 2.2: Stateful:
content: ({ active }: { active: boolean }, previous) => [...previous, 'lorem', 'ipsum', 'dolor', 'sit', active ? 'amet' : null],
footer: ({ active }: { active: boolean }, previous) => [...previous, `footer-is-active:${active}`],
})

Reference

Merging any two class names

To combine 2 class names use mergeClassNames(). It transforms arguments into objects first and than deeply appends non-nested class name values to a special HOST_KEY property while keeping the structure.

import { mergeClassNames } from "@site/../packages/class-name/dist";

const nestedClassName = mergeClassNames(
// 1. Host class name:
({ active }: { active: boolean }, previous) => [...previous, `active:${active}`],
// 2. Sub-component class names:
{
// 2.1: Stateless:
header: 'hello world',
// 2.2: Stateful:
content: ({ active }: { active: boolean }, previous) => [...previous, 'lorem', 'ipsum', 'dolor', 'sit', active ? 'amet' : null],
footer: ({ active }: { active: boolean }, previous) => [...previous, `footer-is-active:${active}`],
},
)
Question: Can I use a different key, e.g. container or & for the host class name property?

You could use any property than HOST_KEY. It you would require you to merge other types of values coming through className (e.g. string) prop with the values coming via nested container property together yourself.

Full example

// 1. Define imports
import type { ClassNameProp } from '@unwind/class-name'
import { defineClassName, mergeClassNames, resolveClassName } from '@unwind/class-name'
import { memo, PropsWithChildren } from 'react'

// 2. Define class name state:
type MyComponentClassNameState = { active: boolean }

// 3. Define host (container) class name:
const myComponentHostClassName = defineClassName(({ active }: MyComponentClassNameState, previous) => [
...previous,
'my-component',
active ? 'my-component-is-active' : null,
])

// 4. Define class name for sub-components and merge with the host class name:
const myComponentClassName = mergeClassNames(myComponentHostClassName, {
icon: ({ active }: MyComponentClassNameState, previous) => [
...previous,
'my-component-icon',
active ? 'my-component-icon-is-active' : null,
],
content: ({ active }: MyComponentClassNameState, previous) => [
...previous,
'my-component-content',
active ? 'my-component-content-is-active' : null,
],
})

type MyComponentProps = PropsWithChildren<{
active?: boolean,
// 5. Define the prop:
className?: ClassNameProp<typeof myComponentClassName>
}>

const MyComponent = memo<MyComponentProps>(({ active = false, children, className }) => {
// 6. Extend your styles with the outer class name:
const styles = mergeClassNames(myComponentClassName, className)

return (
// 7. Resolve string representations:
<div className={resolveClassName({ active }, styles)}>
<span className={resolveClassName({ active }, styles.icon)} />
<div className={resolveClassName({ active }, styles.content)}>
{children}
</div>
</div>
)
})
MyComponent.displayName = 'MyComponent'

HOST_KEY

A HOST_KEY object property is used to hold host class selector values and the other alpha character keys can be used for sub-components.


Representing className in Javascript

Mapping own host element value and values of it's children onto simple object is not 1:1. Using $ is a simple compromise to be able to represent nested class name bearing both class name of the host and sub-components.

In markup languages like XML, HTML, or JSX nodes can have attribute values alongside the children nodes.

Options to mapping nested component tree onto object and vice versa:

1. Fails on either the host element or the children class names

[ hostClassName ]

<Button className={hostClassName}>
<Icon className={iconClassName} />
<Text className={textClassName} />
</Button>


{
icon: iconClassName,
text: textClassName,
}

2. Works with $ for the host element as well as for the children class names:

{
$: [ hostClassName ],
icon: iconClassName,
text: textClassName,
}



<Button className={hostClassName}>
<Icon className={iconClassName} />
<Text className={textClassName} />
</Button>

Why $?

You should never use the literal value of the HOST_KEY directly, use import:
import HOST_KEY from '@unwind/class-name'

Although Javascript is not limited to ASCII set, a $ character:

  1. can be used without quotes in object property identifier (as well as alphanumeric, or _)
  2. can be the first character of in an identifier.
  3. is still quite easy to write (when needed)
  4. is more visible than _

Using a $ instead of a '&'in SASS or Less to denote host (parent) element is just a matter of ergonomics due & would require quotes whan writing or accessing the value.

$
// Creating:
const nested = {
$: ...,
...,
}

// Accessing:
nested.$
vs.
&
// Creating:
const nested = {
'&': ...,
...,
}

// Accessing:
nested['&']

Appendix

See also JavaScript Reference Lexical_grammar Identifiers.