Writing Modifiers
A modifier is a function that takes a StyleRule and returns a new StyleRule with additional selectors or media queries. This page covers creating custom pseudo-state modifiers, media query modifiers, and composing them with when().
Recap: the Modifier signature
import type { Modifier, StyleRule } from 'typewritingclass'
type Modifier = (rule: StyleRule) => StyleRuleA modifier does not modify the CSS declarations. It copies the rule and appends a selector (for pseudo-classes/pseudo-elements) or a media query (for responsive/preference conditions) to the appropriate array on the StyleRule.
Building blocks
Typewriting Class provides two internal functions for constructing modifiers:
wrapWithSelector(rule, selector)— appends a CSS selector string (e.g.,':hover','::before') to the rule’sselectorsarray.wrapWithMediaQuery(rule, query)— appends a media query string (e.g.,'(min-width: 768px)') to the rule’smediaQueriesarray.
Both return a new StyleRule without mutating the original.
Custom pseudo-state modifiers
Single pseudo-class
The simplest modifier wraps a rule with a single pseudo-class:
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithSelector } from 'typewritingclass/rule'
export const visited: Modifier = (rule: StyleRule) => wrapWithSelector(rule, ':visited')
export const checked: Modifier = (rule: StyleRule) => wrapWithSelector(rule, ':checked')
export const placeholder: Modifier = (rule: StyleRule) => wrapWithSelector(rule, '::placeholder')
export const selection: Modifier = (rule: StyleRule) => wrapWithSelector(rule, '::selection')Usage:
import { cx, when, textColor, bg } from 'typewritingclass'import { visited, selection } from './my-modifiers'
const link = cx( textColor('#3b82f6'), when(visited)(textColor('#7c3aed')),)// CSS: .abc { color: #3b82f6; }// .def:visited { color: #7c3aed; }
const content = cx( when(selection)(bg('#dbeafe'), textColor('#1e3a5f')),)// CSS: .ghi::selection { background-color: #dbeafe; color: #1e3a5f; }Structural pseudo-classes
You can create modifiers for any CSS pseudo-class, including functional ones:
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithSelector } from 'typewritingclass/rule'
export const odd: Modifier = (rule: StyleRule) => wrapWithSelector(rule, ':nth-child(odd)')
export const even: Modifier = (rule: StyleRule) => wrapWithSelector(rule, ':nth-child(even)')
export function nthChild(n: string): Modifier { return (rule: StyleRule) => wrapWithSelector(rule, `:nth-child(${n})`)}Notice that nthChild is a modifier factory — a function that returns a modifier. This is the pattern to use when the modifier itself needs a parameter:
import { cx, when, bg } from 'typewritingclass'import { odd, even, nthChild } from './my-modifiers'
const listItem = cx( bg('#ffffff'), when(odd)(bg('#f9fafb')),)// CSS: .abc { background-color: #ffffff; }// .def:nth-child(odd) { background-color: #f9fafb; }
const thirdItem = cx( when(nthChild('3n'))(bg('#dbeafe')),)// CSS: .ghi:nth-child(3n) { background-color: #dbeafe; }Parent and sibling selectors
Modifiers are not limited to pseudo-classes. You can use any CSS selector suffix:
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithSelector } from 'typewritingclass/rule'
// Styles apply when a parent has the `.dark` classexport const darkClass: Modifier = (rule: StyleRule) => wrapWithSelector(rule, '.dark &')
// Styles apply to direct childrenexport const child: (selector: string) => Modifier = (selector) => (rule: StyleRule) => wrapWithSelector(rule, ` > ${selector}`)
// Styles apply when preceded by a siblingexport const afterSibling: Modifier = (rule: StyleRule) => wrapWithSelector(rule, ' + &')Custom media query modifiers
Breakpoints
Create custom breakpoint modifiers for any screen size:
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithMediaQuery } from 'typewritingclass/rule'
export const tablet: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, '(min-width: 600px) and (max-width: 1023px)')
export const desktop: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, '(min-width: 1024px)')
export const widescreen: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, '(min-width: 1920px)')Usage:
import { cx, when, p, gap } from 'typewritingclass'import { tablet, desktop, widescreen } from './my-modifiers'
const layout = cx( p(4), gap(2), when(tablet)(p(6), gap(4)), when(desktop)(p(8), gap(6)), when(widescreen)(p(12), gap(8)),)Custom breakpoint factory
If your project uses a custom set of breakpoints, create a factory:
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithMediaQuery } from 'typewritingclass/rule'
export function minWidth(px: number): Modifier { return (rule: StyleRule) => wrapWithMediaQuery(rule, `(min-width: ${px}px)`)}
export function maxWidth(px: number): Modifier { return (rule: StyleRule) => wrapWithMediaQuery(rule, `(max-width: ${px}px)`)}
export function between(minPx: number, maxPx: number): Modifier { return (rule: StyleRule) => wrapWithMediaQuery(rule, `(min-width: ${minPx}px) and (max-width: ${maxPx}px)`)}Usage:
import { cx, when, p } from 'typewritingclass'import { minWidth, between } from './my-modifiers'
const content = cx( p(4), when(minWidth(1400))(p(12)), when(between(600, 900))(p(6)),)Preference queries
Create modifiers for user preferences beyond dark mode:
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithMediaQuery } from 'typewritingclass/rule'
export const reducedMotion: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, '(prefers-reduced-motion: reduce)')
export const highContrast: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, '(prefers-contrast: high)')
export const print: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, 'print')
export const landscape: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, '(orientation: landscape)')
export const portrait: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, '(orientation: portrait)')
export const coarsePointer: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, '(pointer: coarse)')
export const finePointer: Modifier = (rule: StyleRule) => wrapWithMediaQuery(rule, '(pointer: fine)')Usage:
import { cx, css, when } from 'typewritingclass'import { reducedMotion, print, highContrast } from './my-modifiers'
const animated = cx( css`transition: transform 0.3s ease;`, when(reducedMotion)(css`transition: none;`),)
const content = cx( bg('#ffffff'), when(print)(bg('transparent'), css`color: black;`),)
const border = cx( borderColor('#e5e7eb'), when(highContrast)(borderColor('#000000'), css`border-width: 2px;`),)Composability with when()
All custom modifiers compose naturally with when(), just like the built-in ones. You can stack multiple modifiers:
import { cx, when, bg, p, hover } from 'typewritingclass'import { desktop, reducedMotion } from './my-modifiers'
const card = cx( p(4), bg('#ffffff'), // Hover effect only on desktop when(hover, desktop)(bg('#f3f4f6')), // Disable transitions for reduced motion when(reducedMotion)(css`transition: none;`),)When when() receives multiple modifiers, it applies them right-to-left. So when(hover, desktop) means “at the desktop breakpoint, on hover” — the rule is first wrapped in desktop (adding the media query), then in hover (adding the pseudo-class):
@media (min-width: 1024px) { ._abc:hover { background-color: #f3f4f6; }}Custom modifiers as when() arguments
Since your modifiers satisfy the Modifier type, they are interchangeable with built-in ones in when() calls:
import { cx, when, bg, textColor, hover, md } from 'typewritingclass'import { reducedMotion, finePointer } from './my-modifiers'
const button = cx( bg('#3b82f6'), textColor('#ffffff'), // Only animate on hover, for desktop-like devices with fine pointers when(hover, finePointer)( bg('#2563eb'), css`transform: translateY(-1px); transition: all 0.15s ease;`, ), // Remove animation for reduced motion preference when(reducedMotion)( css`transition: none; transform: none;`, ),)Building compound modifiers
You can compose modifiers into new modifiers that combine multiple conditions:
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithSelector, wrapWithMediaQuery } from 'typewritingclass/rule'
// Hover only on devices with a fine pointer (not touch)export const hoverFine: Modifier = (rule: StyleRule) => wrapWithMediaQuery(wrapWithSelector(rule, ':hover'), '(pointer: fine)')
// Focus styles only when keyboard-navigatingexport const keyboardFocus: Modifier = (rule: StyleRule) => wrapWithSelector(rule, ':focus-visible:not(:hover)')Usage:
import { cx, when, bg } from 'typewritingclass'import { hoverFine, keyboardFocus } from './my-modifiers'
const interactive = cx( bg('#3b82f6'), when(hoverFine)(bg('#2563eb')), when(keyboardFocus)(css`outline: 2px solid #60a5fa; outline-offset: 2px;`),)Generated CSS:
._abc { background-color: #3b82f6; }@media (pointer: fine) { ._def:hover { background-color: #2563eb; } }._ghi:focus-visible:not(:hover) { outline: 2px solid #60a5fa; outline-offset: 2px; }Container query modifiers
Use wrapWithMediaQuery with @container syntax for container query support:
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithMediaQuery } from 'typewritingclass/rule'
export function containerMin(name: string, minWidth: string): Modifier { return (rule: StyleRule) => wrapWithMediaQuery(rule, `(min-width: ${minWidth})`) // Note: container query syntax depends on your browser targets. // You may need to adjust the output format.}Complete example: a responsive utilities module
import type { StyleRule, Modifier } from 'typewritingclass'import { wrapWithMediaQuery, wrapWithSelector } from 'typewritingclass/rule'
// Breakpoints for a custom design systemexport const mobile: Modifier = (rule) => wrapWithMediaQuery(rule, '(max-width: 639px)')
export const tablet: Modifier = (rule) => wrapWithMediaQuery(rule, '(min-width: 640px) and (max-width: 1023px)')
export const desktop: Modifier = (rule) => wrapWithMediaQuery(rule, '(min-width: 1024px)')
export const wide: Modifier = (rule) => wrapWithMediaQuery(rule, '(min-width: 1440px)')
// Preference modifiersexport const reducedMotion: Modifier = (rule) => wrapWithMediaQuery(rule, '(prefers-reduced-motion: reduce)')
export const print: Modifier = (rule) => wrapWithMediaQuery(rule, 'print')
// Interaction modifiersexport const hoverCapable: Modifier = (rule) => wrapWithMediaQuery(rule, '(hover: hover)')
export const touchDevice: Modifier = (rule) => wrapWithMediaQuery(rule, '(hover: none) and (pointer: coarse)')Usage:
import { cx, when, p, gap, css } from 'typewritingclass'import { mobile, tablet, desktop, wide, reducedMotion, print } from './responsive'
const page = cx( p(4), gap(2), when(tablet)(p(6), gap(4)), when(desktop)(p(8), gap(6)), when(wide)(p(12), gap(8)), when(mobile)(css`font-size: 14px;`), when(print)(css`background: none; color: black;`), when(reducedMotion)(css`animation: none; transition: none;`),)