Plugin Concepts
Typewriting Class has no plugin registry, no configuration file, and no provider component. The entire system is built on two function signatures: Utilities and Modifiers. Every built-in function — p(), bg(), hover, md, dark — follows exactly the same pattern that your custom functions will follow.
Philosophy
Traditional CSS frameworks have a concept of “plugins” that register themselves with a central configuration object. You modify a config file, restart your build, and hope the types align. Typewriting Class takes a different approach:
- No registration. A utility is just a function that returns a
StyleRule. Import it and use it. - No configuration. There is no central config file. Functions accept their values directly.
- No providers or context. The system is plain functions in, class names out.
- Type safety for free. Since utilities are TypeScript functions, you get autocompletion, type checking, and refactoring support from your editor.
This means extending Typewriting Class is the same as writing any TypeScript module: export functions from a file, import them where you need them.
The two signatures
Everything in the Typewriting Class ecosystem fits one of two shapes:
Utility: (value) => StyleRule
A utility takes a value and returns a StyleRule containing one or more CSS declarations:
import type { Utility } from 'typewritingclass'
// This is the exact signature of `p`, `bg`, `rounded`, and every other utility:type Utility = (value: any) => StyleRuleExamples of built-in utilities:
import { p, bg, textColor, rounded, opacity } from 'typewritingclass'
p(4) // => StyleRule { declarations: { padding: '1rem' } }bg('#3b82f6') // => StyleRule { declarations: { 'background-color': '#3b82f6' } }textColor('#111827') // => StyleRule { declarations: { color: '#111827' } }rounded('lg') // => StyleRule { declarations: { 'border-radius': '0.5rem' } }opacity('0.5') // => StyleRule { declarations: { opacity: '0.5' } }A StyleRule is a simple data object:
interface StyleRule { _tag: 'StyleRule' declarations: Record<string, string> // CSS property-value pairs selectors: string[] // e.g., [':hover'], [':focus'] mediaQueries: string[] // e.g., ['(min-width: 768px)'] dynamicBindings?: Record<string, string> // e.g., { '--twc-d0': '#ff0000' }}When you pass a StyleRule to cx(), the system hashes the declarations into a unique class name, registers the CSS, and returns the class string.
Modifier: (rule: StyleRule) => StyleRule
A modifier takes a StyleRule and returns a new StyleRule with additional selectors or media queries:
import type { Modifier } from 'typewritingclass'
// This is the exact signature of `hover`, `focus`, `md`, `dark`, and every other modifier:type Modifier = (rule: StyleRule) => StyleRuleExamples of built-in modifiers:
import { hover, focus, md, dark } from 'typewritingclass'import { p, bg } from 'typewritingclass'
hover(bg('#2563eb'))// => StyleRule {// declarations: { 'background-color': '#2563eb' },// selectors: [':hover'],// mediaQueries: [],// }
md(p(8))// => StyleRule {// declarations: { padding: '2rem' },// selectors: [],// mediaQueries: ['(min-width: 768px)'],// }A modifier does not change the declarations. It copies the rule and appends a selector or media query to the appropriate array.
How they compose
Utilities and modifiers compose through cx() and when():
import { cx, when, p, bg, hover, md } from 'typewritingclass'
const className = cx( p(4), // Utility: padding bg('#3b82f6'), // Utility: background when(hover)(bg('#2563eb')), // Modifier(Utility): hover background when(md)(p(8)), // Modifier(Utility): responsive padding when(hover, md)(bg('#1d4ed8')), // Stacked modifiers: hover + responsive)when() accepts one or more modifiers and returns a function that accepts style rules. The modifiers are applied right-to-left, so when(hover, md) means “at the md breakpoint, on hover.”
This composability is the core insight: because both utilities and modifiers operate on the same StyleRule data structure, they can be freely combined without any special glue code.
Built-ins follow the same pattern
This is important to internalize: there is nothing special about the built-in utilities and modifiers. They are defined using the exact same primitives you will use to write your own.
Here is the actual source code of the built-in hover modifier:
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithSelector } from 'typewritingclass/rule'
export const hover: Modifier = (rule: StyleRule) => wrapWithSelector(rule, ':hover')And here is the actual source code of the built-in bg utility (simplified):
import type { StyleRule } from 'typewritingclass'import { createRule } from 'typewritingclass/rule'
export function bg(color: string): StyleRule { return createRule({ 'background-color': color })}Your custom utilities and modifiers are first-class citizens. They have the same capabilities, the same performance characteristics, and the same composability as the built-ins.
The building blocks
Typewriting Class provides four low-level functions for constructing StyleRule objects:
| Function | Purpose |
|---|---|
createRule(declarations) | Creates a static StyleRule from CSS property-value pairs |
createDynamicRule(declarations, bindings) | Creates a StyleRule with dynamic CSS custom property bindings |
wrapWithSelector(rule, selector) | Returns a copy of the rule with a CSS selector appended |
wrapWithMediaQuery(rule, query) | Returns a copy of the rule wrapped in a media query |
These four functions are all you need to build any utility or modifier. The following pages walk through each in detail:
- Writing Utilities — creating custom utility functions with
createRuleandcreateDynamicRule. - Writing Modifiers — creating custom modifier functions with
wrapWithSelectorandwrapWithMediaQuery. - Recipes & Composites — composing multiple utilities and modifiers into reusable higher-level abstractions.
No build step required
Because plugins are just functions, they work immediately. There is no registration, no build step, and no configuration change needed. Write a function, import it, pass it to cx(). The compiler handles everything else.
import type { StyleRule } from 'typewritingclass'import { createRule } from 'typewritingclass/rule'
export function textShadow(value: string): StyleRule { return createRule({ 'text-shadow': value })}
// App.tsximport { cx, p } from 'typewritingclass'import { textShadow } from './my-utilities'
const heading = cx(p(4), textShadow('2px 2px 4px rgba(0,0,0,0.3)'))// Just works. No registration. No config. No restart.Sharing via npm
Since plugins are plain TypeScript modules, publishing them to npm is straightforward:
- Create a package with your utility and modifier functions.
- Export them from the package’s entry point.
- Publish to npm.
- Consumers
bun add your-packageand import the functions.
No peer dependency on a specific typewritingclass version is needed beyond the StyleRule type, which is stable. See Recipes & Composites for patterns around packaging and distributing reusable style compositions.