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:
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:
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:
| Property | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for the plugin |
order | 'pre' | 'post' | No | Controls execution order relative to other plugins |
| hook functions | — | No | Functions called at various lifecycle stages |
Execution Order
Plugins are sorted by order before hooks run. The execution sequence is: pre → (default) → post.
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.
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.
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.
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:
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:
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.
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:
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:
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:
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:
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:
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',
)
},
})| Method | Purpose |
|---|---|
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:
| API | Description |
|---|---|
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.config | Access the resolved engine config |
engine.store.atomicStyleIds | Map of content hash → atomic style ID |
engine.store.atomicStyles | Map of ID → AtomicStyle object |
Real-World Examples
Reset Plugin (simplified)
Based on @pikacss/plugin-reset — uses order: 'pre', configureRawConfig, module augmentation, and engine.addPreflight():
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:
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:
# Create a new plugin package interactively
pnpm newplugin
# Or pass the plugin name directly
pnpm newplugin my-featureThe scaffold generates:
- Package folder at
packages/plugin-<name>/ src/index.tswith adefineEnginePlugintemplatepackage.jsonwith@pikacss/coreas 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
| Type | Package name pattern |
|---|---|
| Official | @pikacss/plugin-xxx |
| Community | pikacss-plugin-xxx |
- Export a factory function that returns
EnginePlugin - Use module augmentation for config type extension
- Include
pikacssandpikacss-plugininpackage.jsonkeywords