# API reference

> Look up supported class inputs, variant types, defaults, and customization signatures in @hulla/style.

Look up the inputs accepted by `cn()`, the types inferred by `vn()`, and the options for `style()`. For a worked example, follow [Style a button](https://hulla.dev/guides/style/quick-start.md).

## Create the helpers

```ts
import { style } from '@hulla/style'

const { cn, vn } = style()
```

Both configuration properties are optional. `serializer` defaults to `defaultSerializer`; `composer` defaults to `defaultComposer`. The returned `cn` and `vn` share the same configured pipeline. `style()` does not generate CSS or know framework-specific class props.

## Pass supported class inputs

`ClassName` is a non-recursive class input or a conditional object. The source type is `ClassNameNonRecursive | Record<string, ClassNameNonRecursive>`; `ClassNameNonRecursive` is an internal helper, not a public export:

```ts
import type {
  ClassName,
  Composer,
  Serializer,
  StyleConfig,
  Variant,
  VariantsDefinition,
} from '@hulla/style'
```

`ClassName` accepts strings, `string[]`, numbers, booleans, nullish values, `0n`, and conditional objects. Arrays must contain strings: nested arrays and boolean array members are outside the declared type.

The default serializer produces these values at runtime:

| Input                                                | Serialized output                                    |
| ---------------------------------------------------- | ---------------------------------------------------- |
| `'base px-2'`                                        | `'base px-2'`                                        |
| `['base', 'px-2']`                                   | `'base px-2'`                                        |
| `{ active: true, hidden: false }`                    | `'active'`                                           |
| `new Set(['base', 'px-2'])`                          | `'base px-2'`                                        |
| `new Map([['base', 'active']])`                      | `'base active'` (entry key and value are serialized) |
| `''`, `0`, `0n`, `NaN`, `false`, `null`, `undefined` | `''`                                                 |
| Any non-zero number, such as `42`                    | Stringified numeric text                             |
| `1n`                                                 | `''` (bigint values are not serialized)              |
| `true`                                               | `''`                                                 |

> **Note: Some runtime inputs aren't accepted by TypeScript**
>
> The serializer handles Set and Map, but the public `ClassName` type doesn't include them. Prefer string arrays and conditional objects in typed code. Non-zero bigints such as `1n` are also excluded; at runtime all bigints serialize to an empty string.

## Understand `cn` and the defaults

`cn(...classes)` serializes each argument, then passes the resulting strings to the composer. With the default composer, empty inputs disappear and repeated class names appear only once, in their original order:

```ts
cn('rounded px-2', 'px-2 text-sm') // 'rounded px-2 text-sm'
cn(42, NaN) // '42'
cn('px-2', 'px-4') // 'px-2 px-4'
```

Exact-token deduplication is not CSS conflict resolution. `defaultComposer` keeps both different spacing classes. Configure a utility-aware composer such as `twMerge` if the later conflicting utility should win; `cn` itself does not interpret CSS.

## Define a variant

`vn(variants)` maps each key to a class string. Its return type, `Variant<V>`, is a callable function with an `.infer` property typed as the keys of your map:

```ts
const $size = vn({ compact: 'px-2 text-sm', roomy: ['px-4', 'text-base'] })
type Size = typeof $size.infer // 'compact' | 'roomy'
const classes = $size('compact')
```

`.infer` exists for TypeScript and is `undefined` at runtime. Call the variant function to get classes.

There is no default selection in `vn()`. Set a default in your component's props when callers can omit the choice. Variant classes use the same serializer and composer as `cn()`.

## Customize serialization and composition

The exported signatures are `Serializer = (input: ClassName) => string` and `Composer = (...strings: string[]) => string`; both are synchronous. This runnable example imports the default serializer and changes each input to uppercase before a simple joining composer receives it:

```ts
import { defaultSerializer, style } from '@hulla/style'

const { cn } = style({
  serializer: (input) => defaultSerializer(input).toUpperCase(),
  composer: (...strings) => strings.filter(Boolean).join(' '),
})

cn('base', { active: true }) // 'BASE ACTIVE'
```

A custom serializer receives **every** `cn` argument, including strings, and runs on variant input too. Return one string per input position.

A custom composer receives those strings in order and decides how to combine them: preserve duplicates, normalize whitespace, or resolve conflicting utilities. Both hooks are synchronous.

`VariantsDefinition` describes a variant map, `Variant<V>` types its callable function and `.infer`, and `StyleConfig` holds the two hooks. See [Custom merging](https://hulla.dev/docs/style/custom-merging.md) for a Tailwind example.

The package exports `style(config?: StyleConfig)`, `defaultSerializer(input: ClassName): string`, `defaultComposer(...strings: string[]): string`, and the public types `ClassName`, `Serializer`, `Composer`, `VariantsDefinition`, `StyleConfig`, and `Variant`. The `Variant<V>` shape is `((prop: keyof V) => string) & { infer: keyof V }`.
