Skip to content

Create a Plugin

This guide walks you through creating a PikaCSS plugin from scratch. Plugins extend the engine with custom variables, shortcuts, selectors, keyframes, preflights, and autocomplete entries — all with full TypeScript type safety.

Minimal Plugin

A plugin is a plain object with a name and optional hooks, wrapped in defineEnginePlugin() for type safety:

ts
import type { EnginePlugin } from '@pikacss/core'
import { defineEnginePlugin } from '@pikacss/core'

export function myPlugin(): EnginePlugin {
	return defineEnginePlugin({
		name: 'my-plugin',
	})
}

The convention is to export a factory function that returns the plugin object. This allows users to pass options:

ts
import type { EnginePlugin } from '@pikacss/core'
import { defineEnginePlugin } from '@pikacss/core'

export interface GreetingPluginOptions {
	/** The greeting prefix. @default 'Hello' */
	prefix?: string
}

export function greetingPlugin(
	options: GreetingPluginOptions = {},
): EnginePlugin {
	const { prefix = 'Hello' } = options

	return defineEnginePlugin({
		name: 'greeting',
		configureEngine: async (engine) => {
			// Use options to customize plugin behavior
			engine.addPreflight(
				`/* ${prefix} from greeting plugin */`,
			)
		},
	})
}

Plugin Structure

Every plugin has:

PropertyTypeRequiredDescription
namestringYesUnique identifier for the plugin
order'pre' | 'post'NoControls execution order relative to other plugins
hook functionsNoFunctions called at various lifecycle stages

Execution Order

Plugins are sorted by order before hooks run. The execution sequence is: pre → (default) → post.

ts
import { defineEnginePlugin } from '@pikacss/core'

// Runs before default-order plugins
export const earlyPlugin = defineEnginePlugin({
	name: 'early',
	order: 'pre',
})

// Runs in default order (no `order` specified)
export const normalPlugin = defineEnginePlugin({
	name: 'normal',
})

// Runs after default-order plugins
export const latePlugin = defineEnginePlugin({
	name: 'late',
	order: 'post',
})

Lifecycle Hooks

During createEngine(config), hooks fire in this order:

1. configureRawConfig (async)

Modify the raw config before it is resolved. Return the modified config to pass it to the next plugin.

ts
import { defineEnginePlugin } from '@pikacss/core'

export const plugin = defineEnginePlugin({
	name: 'example',

	// Async — modify raw config before resolution
	configureRawConfig(config) {
		// Add default preflights
		config.preflights ??= []
		config.preflights.push('/* injected by example plugin */')
		return config
	},

	// Sync — read finalized raw config (cannot modify)
	rawConfigConfigured(config) {
		console.log('Final prefix:', config.prefix)
	},
})

2. rawConfigConfigured (sync)

Called after all configureRawConfig hooks have run. Use this to read the finalized raw config. Return value is ignored.

3. configureResolvedConfig (async)

Modify the resolved config after default values and plugin resolution have been applied.

ts
import { defineEnginePlugin } from '@pikacss/core'

export const plugin = defineEnginePlugin({
	name: 'example',

	// Async — modify resolved config before engine creation
	configureResolvedConfig(resolvedConfig) {
		// Override the prefix in resolved config
		resolvedConfig.prefix = 'x-'
		return resolvedConfig
	},
})

4. configureEngine (async)

The most commonly used hook. Called after the engine is fully constructed. Use this to add variables, shortcuts, selectors, keyframes, preflights, and autocomplete entries.

ts
import { defineEnginePlugin } from '@pikacss/core'

export const plugin = defineEnginePlugin({
	name: 'example',

	// Async — called after engine is created
	configureEngine: async (engine) => {
		// Add CSS variables
		engine.variables.add({
			'--brand-color': '#0ea5e9',
		})

		// Add shortcuts
		engine.shortcuts.add([
			'flex-center',
			{
				display: 'flex',
				alignItems: 'center',
				justifyContent: 'center',
			},
		])

		// Add custom selectors
		engine.selectors.add(['hover', '$:hover'])

		// Add keyframe animations
		engine.keyframes.add([
			'fade-in',
			{ from: { opacity: '0' }, to: { opacity: '1' } },
			['fade-in 0.3s ease'],
		])

		// Add preflight CSS
		engine.addPreflight('*, *::before, *::after { box-sizing: border-box; }')
	},
})

Transform Hooks (runtime)

These hooks are called during style extraction at runtime:

ts
import { defineEnginePlugin } from '@pikacss/core'

export const plugin = defineEnginePlugin({
	name: 'example',

	// Transform selectors before they are resolved
	async transformSelectors(selectors) {
		return selectors.map(s =>
			s === 'dark' ? '[data-theme="dark"]' : s,
		)
	},

	// Transform style items before extraction
	async transformStyleItems(styleItems) {
		// Insert additional style items or modify existing ones
		return styleItems
	},

	// Transform style definitions before extraction
	async transformStyleDefinitions(styleDefinitions) {
		return styleDefinitions
	},
})

Notification Hooks (sync)

These hooks notify plugins about state changes — they cannot modify payloads:

ts
import { defineEnginePlugin } from '@pikacss/core'

export const plugin = defineEnginePlugin({
	name: 'example',

	// Sync — called when a new atomic style is generated
	atomicStyleAdded(atomicStyle) {
		console.log(`New atomic style: ${atomicStyle.id}`)
	},

	// Sync — called when preflights are updated
	preflightUpdated() {
		console.log('Preflights updated')
	},

	// Sync — called when autocomplete config changes
	autocompleteConfigUpdated() {
		console.log('Autocomplete config updated')
	},
})

Hook Execution Model

  • Hooks run plugin-by-plugin in sorted order.
  • If an async hook returns a non-null value, that value replaces the payload for the next plugin.
  • Hook errors are caught and logged; execution continues to the next plugin.

Module Augmentation

Use TypeScript module augmentation to add custom config options that are type-safe for end users. This is how the official plugins (icons, reset, typography) add their config fields to EngineConfig.

ts
import type { EnginePlugin } from '@pikacss/core'
import { defineEnginePlugin } from '@pikacss/core'

// Step 1: Define your config type
export interface SpacingConfig {
	/** Base spacing unit in px. @default 4 */
	base?: number
}

// Step 2: Augment EngineConfig so users get autocomplete
declare module '@pikacss/core' {
	interface EngineConfig {
		spacing?: SpacingConfig
	}
}

// Step 3: Read the config in your plugin
export function spacingPlugin(): EnginePlugin {
	let spacingConfig: SpacingConfig = {}

	return defineEnginePlugin({
		name: 'spacing',

		configureRawConfig(config) {
			if (config.spacing)
				spacingConfig = config.spacing
		},

		configureEngine: async (engine) => {
			const base = spacingConfig.base ?? 4

			// Generate spacing variables
			for (let i = 0; i <= 12; i++) {
				engine.variables.add({
					[`--spacing-${i}`]: `${i * base}px`,
				})
			}

			// Add spacing shortcuts
			for (let i = 0; i <= 12; i++) {
				engine.shortcuts.add([
					`p-${i}`,
					{ padding: `var(--spacing-${i})` },
				])
				engine.shortcuts.add([
					`m-${i}`,
					{ margin: `var(--spacing-${i})` },
				])
			}
		},
	})
}

Users then get full autocomplete when configuring the engine:

ts
import { defineEngineConfig } from '@pikacss/core'
import { icons } from '@pikacss/plugin-icons'
import { reset } from '@pikacss/plugin-reset'
import { typography } from '@pikacss/plugin-typography'

export default defineEngineConfig({
	plugins: [
		reset(),
		icons(),
		typography(),
	],
	// Plugin config options are type-safe via module augmentation
	reset: 'modern-normalize',
	icons: { prefix: 'i-', scale: 1.2 },
	typography: {},
})

Adding Preflights

Preflights are global CSS styles injected before atomic styles. The engine.addPreflight() method accepts three forms:

String Preflight

Raw CSS string injected as-is:

ts
import { defineEnginePlugin } from '@pikacss/core'

export const plugin = defineEnginePlugin({
	name: 'example',
	configureEngine: async (engine) => {
		// String preflight — raw CSS injected as-is
		engine.addPreflight(`
      *, *::before, *::after {
        box-sizing: border-box;
      }
      body {
        margin: 0;
        line-height: 1.5;
      }
    `)
	},
})

PreflightDefinition Object

A structured object with selectors as keys and CSS properties as values:

ts
import type { PreflightDefinition } from '@pikacss/core'
import { defineEnginePlugin } from '@pikacss/core'

export const plugin = defineEnginePlugin({
	name: 'example',
	configureEngine: async (engine) => {
		// PreflightDefinition — structured object with CSS properties
		const preflight: PreflightDefinition = {
			':root': {
				fontSize: '16px',
				lineHeight: '1.5',
			},
			'body': {
				margin: '0',
				fontFamily: 'system-ui, sans-serif',
			},
		}
		engine.addPreflight(preflight)
	},
})

PreflightFn Function

A function that receives the engine instance and returns a string or PreflightDefinition. Useful for dynamic preflights that read engine state:

ts
import { defineEnginePlugin } from '@pikacss/core'

export const plugin = defineEnginePlugin({
	name: 'example',
	configureEngine: async (engine) => {
		// PreflightFn — dynamic preflight that reads engine state
		engine.addPreflight((engine, isFormatted) => {
			const vars = Array.from(engine.variables.store.entries())
			const css = vars
				.map(([name, list]) =>
					list.map(v => `${name}: ${v.value};`)
						.join(
							isFormatted ? '\n  ' : '',
						),
				)
				.join(isFormatted ? '\n  ' : '')

			return isFormatted
				? `:root {\n  ${css}\n}`
				: `:root{${css}}`
		})
	},
})

Autocomplete API

Plugins can enrich the TypeScript autocomplete experience by adding custom entries. These APIs are available on the engine instance inside configureEngine:

ts
import { defineEnginePlugin } from '@pikacss/core'

export const plugin = defineEnginePlugin({
	name: 'example',
	configureEngine: async (engine) => {
		// Add custom selectors to autocomplete
		engine.appendAutocompleteSelectors('hover', 'focus', 'dark')

		// Add custom style item strings to autocomplete
		engine.appendAutocompleteStyleItemStrings(
			'flex-center',
			'btn-primary',
		)

		// Add extra TypeScript properties for autocomplete
		engine.appendAutocompleteExtraProperties('__shortcut')

		// Add extra CSS properties for autocomplete
		engine.appendAutocompleteExtraCssProperties(
			'--my-color',
			'--my-size',
		)

		// Add TypeScript type unions for a property value
		engine.appendAutocompletePropertyValues(
			'__shortcut',
			'(string & {})',
		)

		// Add concrete CSS values for a property
		engine.appendAutocompleteCssPropertyValues(
			'display',
			'flex',
			'grid',
			'block',
		)
	},
})
MethodPurpose
appendAutocompleteSelectors(...selectors)Add selector strings to autocomplete
appendAutocompleteStyleItemStrings(...strings)Add style item strings (shortcut names, etc.)
appendAutocompleteExtraProperties(...properties)Add extra TypeScript properties
appendAutocompleteExtraCssProperties(...properties)Add extra CSS properties (e.g. custom CSS variables)
appendAutocompletePropertyValues(property, ...tsTypes)Add TypeScript type unions for a property's value
appendAutocompleteCssPropertyValues(property, ...values)Add concrete CSS values for a CSS property

Built-in Engine APIs

Inside configureEngine, the engine exposes APIs from the built-in core plugins:

APIDescription
engine.variables.add(definition)Add CSS variables with autocomplete support
engine.shortcuts.add(...shortcuts)Add static or dynamic shortcuts
engine.selectors.add(...selectors)Add static or dynamic selector mappings
engine.keyframes.add(...keyframes)Add @keyframes animations
engine.addPreflight(preflight)Add global preflight CSS
engine.configAccess the resolved engine config
engine.store.atomicStyleIdsMap of content hash → atomic style ID
engine.store.atomicStylesMap of ID → AtomicStyle object

Real-World Examples

Reset Plugin (simplified)

Based on @pikacss/plugin-reset — uses order: 'pre', configureRawConfig, module augmentation, and engine.addPreflight():

ts
import type { EnginePlugin } from '@pikacss/core'
import { defineEnginePlugin } from '@pikacss/core'

export type ResetStyle = 'modern-normalize' | 'eric-meyer'

// Module augmentation for config type safety
declare module '@pikacss/core' {
	interface EngineConfig {
		reset?: ResetStyle
	}
}

// Simplified version of @pikacss/plugin-reset
export function reset(): EnginePlugin {
	let style: ResetStyle = 'modern-normalize'

	return defineEnginePlugin({
		name: 'reset',
		order: 'pre', // Run before other plugins

		configureRawConfig: (config) => {
			if (config.reset)
				style = config.reset
		},

		configureEngine: async (engine) => {
			// Load and inject the selected reset stylesheet
			const resetCss = await loadResetCss(style)
			engine.addPreflight(resetCss)
		},
	})
}

async function loadResetCss(style: ResetStyle): Promise<string> {
	// In real code, this loads from bundled CSS files
	return `/* ${style} reset styles */`
}

Typography Plugin (simplified)

Based on @pikacss/plugin-typography — uses variables, shortcuts, and module augmentation:

ts
import type { EnginePlugin } from '@pikacss/core'
import { defineEnginePlugin } from '@pikacss/core'

export interface TypographyPluginOptions {
	/** Custom variable overrides */
	variables?: Record<string, string>
}

// Module augmentation
declare module '@pikacss/core' {
	interface EngineConfig {
		typography?: TypographyPluginOptions
	}
}

// Simplified version of @pikacss/plugin-typography
export function typography(): EnginePlugin {
	let typographyConfig: TypographyPluginOptions = {}

	return defineEnginePlugin({
		name: 'typography',

		configureRawConfig: (config) => {
			if (config.typography)
				typographyConfig = config.typography
		},

		configureEngine: async (engine) => {
			// 1. Add CSS variables
			engine.variables.add({
				'--prose-font-size': '1rem',
				'--prose-line-height': '1.75',
				...typographyConfig.variables,
			})

			// 2. Add base prose shortcut
			engine.shortcuts.add([
				'prose',
				{
					fontSize: 'var(--prose-font-size)',
					lineHeight: 'var(--prose-line-height)',
					maxWidth: '65ch',
				},
			])

			// 3. Add size variant shortcuts
			const sizes = {
				sm: { fontSize: '0.875rem', lineHeight: '1.71' },
				lg: { fontSize: '1.125rem', lineHeight: '1.77' },
			}
			for (const [size, overrides] of Object.entries(sizes)) {
				engine.shortcuts.add([
					`prose-${size}`,
					['prose', overrides],
				])
			}
		},
	})
}

Scaffold a Plugin Package

The monorepo includes an interactive scaffolding script:

bash
# Create a new plugin package interactively
pnpm newplugin

# Or pass the plugin name directly
pnpm newplugin my-feature

The scaffold generates:

  • Package folder at packages/plugin-<name>/
  • src/index.ts with a defineEnginePlugin template
  • package.json with @pikacss/core as a peer dependency
  • TypeScript and Vitest config files
  • A basic test file

The generated factory function follows the pattern create<PascalName>Plugin(options) with the plugin name set to the slug.

Publishing Conventions

TypePackage name pattern
Official@pikacss/plugin-xxx
Communitypikacss-plugin-xxx
  • Export a factory function that returns EnginePlugin
  • Use module augmentation for config type extension
  • Include pikacss and pikacss-plugin in package.json keywords

Next