Writing Utilities
A utility is a function that takes a value and returns a StyleRule. This page covers everything you need to create your own utilities, from simple single-property helpers to dynamic-aware, type-safe, theme-integrated functions ready for npm.
createRule() — static utilities
The simplest way to create a utility is with createRule(). It takes a Record<string, string> of CSS property-value pairs and returns a StyleRule:
import type { StyleRule } from 'typewritingclass'import { createRule } from 'typewritingclass/rule'
export function textShadow(value: string): StyleRule { return createRule({ 'text-shadow': value })}Usage:
import { cx } from 'typewritingclass'import { textShadow } from './my-utilities'
const heading = cx(textShadow('2px 2px 4px rgba(0,0,0,0.3)'))// CSS: text-shadow: 2px 2px 4px rgba(0,0,0,0.3);Multiple declarations
A single utility can set multiple CSS properties. This is common for shorthand-like utilities:
import type { StyleRule } from 'typewritingclass'import { createRule } from 'typewritingclass/rule'
export function truncate(): StyleRule { return createRule({ overflow: 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap', })}
export function absoluteFill(): StyleRule { return createRule({ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', })}Usage:
import { cx, p } from 'typewritingclass'import { truncate, absoluteFill } from './my-utilities'
const label = cx(p(2), truncate())const overlay = cx(absoluteFill())Computed values
Utilities can compute CSS values from their inputs:
import type { StyleRule } from 'typewritingclass'import { createRule } from 'typewritingclass/rule'
export function gridCols(count: number): StyleRule { return createRule({ display: 'grid', 'grid-template-columns': `repeat(${count}, minmax(0, 1fr))`, })}
export function aspectRatio(width: number, height: number): StyleRule { return createRule({ 'aspect-ratio': `${width} / ${height}`, })}
export function lineClamp(lines: number): StyleRule { return createRule({ display: '-webkit-box', '-webkit-line-clamp': String(lines), '-webkit-box-orient': 'vertical', overflow: 'hidden', })}Usage:
import { cx, gap } from 'typewritingclass'import { gridCols, aspectRatio, lineClamp } from './my-utilities'
const photoGrid = cx(gridCols(3), gap(4))const thumbnail = cx(aspectRatio(16, 9))const preview = cx(lineClamp(3))createDynamicRule() — dynamic-aware utilities
If your utility should accept DynamicValue inputs (values that change at runtime), use createDynamicRule() alongside the isDynamic() type guard:
import type { StyleRule } from 'typewritingclass'import type { DynamicValue } from 'typewritingclass'import { createRule, createDynamicRule } from 'typewritingclass/rule'import { isDynamic } from 'typewritingclass'
export function textShadow(value: string | DynamicValue): StyleRule { if (isDynamic(value)) { return createDynamicRule( { 'text-shadow': `var(${value.__id})` }, { [value.__id]: String(value.__value) }, ) } return createRule({ 'text-shadow': value })}The pattern is consistent across all dynamic-aware utilities:
- Check if the input
isDynamic(). - If dynamic: use
createDynamicRule(), referencevar(${value.__id})in the declaration, and mapvalue.__idtoString(value.__value)in the bindings. - If static: use
createRule()with the value directly.
Usage with dcx():
import { dcx, dynamic } from 'typewritingclass'import { textShadow } from './my-utilities'
// Static -- goes into stylesheet directlycx(textShadow('2px 2px 4px rgba(0,0,0,0.3)'))
// Dynamic -- emits var() in stylesheet, value in inline styleconst { className, style } = dcx(textShadow(dynamic('2px 2px 4px rgba(0,0,0,0.3)')))Multiple declarations with dynamic values
When a utility sets multiple properties from a single dynamic value, reference the same var() in each declaration:
import type { StyleRule } from 'typewritingclass'import type { DynamicValue } from 'typewritingclass'import { createRule, createDynamicRule } from 'typewritingclass/rule'import { isDynamic } from 'typewritingclass'
export function square(size: string | DynamicValue): StyleRule { if (isDynamic(size)) { return createDynamicRule( { width: `var(${size.__id})`, height: `var(${size.__id})` }, { [size.__id]: String(size.__value) }, ) } return createRule({ width: size, height: size })}Accepting theme tokens
Typewriting Class theme tokens are branded strings. You can accept them in your utilities for type-safe theme integration:
import type { StyleRule, CSSColor } from 'typewritingclass'import type { DynamicValue } from 'typewritingclass'import { createRule, createDynamicRule } from 'typewritingclass/rule'import { isDynamic } from 'typewritingclass'
export function highlight(color: CSSColor | string | DynamicValue): StyleRule { if (isDynamic(color)) { return createDynamicRule( { 'background-color': `var(${color.__id})`, color: 'inherit' }, { [color.__id]: String(color.__value) }, ) } return createRule({ 'background-color': String(color), color: 'inherit' })}Usage with theme tokens:
import { cx } from 'typewritingclass'import { yellow } from 'typewritingclass/theme/colors'import { highlight } from './my-utilities'
const marked = cx(highlight(yellow[200]))Because yellow[200] carries the CSSColor brand, TypeScript will accept it. Plain strings like '#fef08a' also work since the union includes string.
Type safety with branded types
For maximum type safety, you can define your own branded types to prevent misuse:
import type { Brand, StyleRule } from 'typewritingclass'import { createRule } from 'typewritingclass/rule'
// Define a branded type for animation durationtype Duration = Brand<string, 'duration'>
// Create token valuesexport const fast: Duration = '150ms' as Durationexport const normal: Duration = '300ms' as Durationexport const slow: Duration = '500ms' as Duration
// The utility only accepts Duration-branded valuesexport function transitionDuration(value: Duration): StyleRule { return createRule({ 'transition-duration': value })}Usage:
import { cx } from 'typewritingclass'import { transitionDuration, fast, normal } from './my-utilities'
cx(transitionDuration(fast)) // OKcx(transitionDuration(normal)) // OKcx(transitionDuration('300ms')) // Type error -- not brandedThis prevents accidentally passing a spacing value where a duration is expected, or vice versa.
Utilities with multiple parameters
Nothing limits a utility to a single parameter. You can accept multiple values:
import type { StyleRule } from 'typewritingclass'import { createRule } from 'typewritingclass/rule'
export function transition( property: string, duration: string = '150ms', easing: string = 'ease',): StyleRule { return createRule({ 'transition-property': property, 'transition-duration': duration, 'transition-timing-function': easing, })}Usage:
import { cx, bg, when, hover } from 'typewritingclass'import { transition } from './my-utilities'
const button = cx( bg('#3b82f6'), transition('background-color', '200ms', 'ease-in-out'), when(hover)(bg('#2563eb')),)Utilities with union/enum parameters
Use TypeScript union types or string literal unions for utilities that accept a fixed set of values:
import type { StyleRule } from 'typewritingclass'import { createRule } from 'typewritingclass/rule'
type TextTransformValue = 'uppercase' | 'lowercase' | 'capitalize' | 'none'
export function textTransform(value: TextTransformValue): StyleRule { return createRule({ 'text-transform': value })}
type CursorValue = 'pointer' | 'default' | 'wait' | 'text' | 'move' | 'not-allowed' | 'grab'
export function cursor(value: CursorValue): StyleRule { return createRule({ cursor: value })}TypeScript will provide autocompletion for the allowed values and error on invalid ones.
Utilities with conditional logic
Utilities can contain arbitrary logic. Use this for utilities that map between different value formats:
import type { StyleRule } from 'typewritingclass'import { createRule } from 'typewritingclass/rule'
const spacingScale: Record<number, string> = { 0: '0', 0.5: '0.125rem', 1: '0.25rem', 2: '0.5rem', 3: '0.75rem', 4: '1rem', 6: '1.5rem', 8: '2rem', 12: '3rem', 16: '4rem',}
export function inset(value: number | string): StyleRule { const resolved = typeof value === 'number' ? spacingScale[value] ?? `${value * 0.25}rem` : value
return createRule({ top: resolved, right: resolved, bottom: resolved, left: resolved, })}Publishing as an npm package
Custom utilities are plain TypeScript exports, so publishing them is straightforward.
Package structure
my-twc-utilities/ src/ index.ts # re-exports everything text.ts # text-related utilities layout.ts # layout-related utilities animation.ts # animation-related utilities package.json tsconfig.jsonpackage.json
{ "name": "my-twc-utilities", "version": "1.0.0", "type": "module", "exports": { ".": { "types": "./src/index.ts", "default": "./src/index.ts" } }, "peerDependencies": { "typewritingclass": ">=0.2.0" }}Entry point
export { textShadow, truncate, lineClamp } from './text'export { gridCols, aspectRatio, absoluteFill } from './layout'export { transition, transitionDuration } from './animation'Consumer usage
import { cx, p, bg } from 'typewritingclass'import { textShadow, gridCols, transition } from 'my-twc-utilities'
const card = cx( p(6), bg('#fff'), textShadow('0 1px 2px rgba(0,0,0,0.1)'), transition('all', '200ms'),)The consumer does not need to register anything. They import the functions and pass them to cx(). The Typewriting Class compiler handles the rest.
Complete example: a gradient utility
Here is a complete, production-ready utility with type safety, dynamic support, and documentation:
import type { StyleRule } from 'typewritingclass'import type { DynamicValue } from 'typewritingclass'import { createRule, createDynamicRule } from 'typewritingclass/rule'import { isDynamic } from 'typewritingclass'
type GradientDirection = | 'to-t' | 'to-tr' | 'to-r' | 'to-br' | 'to-b' | 'to-bl' | 'to-l' | 'to-tl'
const directionMap: Record<GradientDirection, string> = { 'to-t': 'to top', 'to-tr': 'to top right', 'to-r': 'to right', 'to-br': 'to bottom right', 'to-b': 'to bottom', 'to-bl': 'to bottom left', 'to-l': 'to left', 'to-tl': 'to top left',}
/** * Creates a linear gradient background. * * @param direction - The gradient direction (e.g., 'to-r', 'to-b'). * @param from - Start color (string or DynamicValue). * @param to - End color (string or DynamicValue). */export function gradient( direction: GradientDirection, from: string | DynamicValue, to: string | DynamicValue,): StyleRule { const dir = directionMap[direction] const bindings: Record<string, string> = {} let fromStr: string let toStr: string
if (isDynamic(from)) { bindings[from.__id] = String(from.__value) fromStr = `var(${from.__id})` } else { fromStr = from }
if (isDynamic(to)) { bindings[to.__id] = String(to.__value) toStr = `var(${to.__id})` } else { toStr = to }
const value = `linear-gradient(${dir}, ${fromStr}, ${toStr})`
if (Object.keys(bindings).length > 0) { return createDynamicRule({ background: value }, bindings) } return createRule({ background: value })}Usage:
import { cx } from 'typewritingclass'import { blue, purple } from 'typewritingclass/theme/colors'import { gradient } from './gradient'
const banner = cx(gradient('to-r', blue[500], purple[600]))// CSS: background: linear-gradient(to right, #3b82f6, #9333ea);With dynamic colors:
import { dcx, dynamic } from 'typewritingclass'import { gradient } from './gradient'
const userStart = dynamic('#ff0000')const userEnd = dynamic('#0000ff')
const { className, style } = dcx(gradient('to-r', userStart, userEnd))// CSS: background: linear-gradient(to right, var(--twc-d0), var(--twc-d1));// style: { '--twc-d0': '#ff0000', '--twc-d1': '#0000ff' }