Skip to content

Selectors

The core:selectors plugin resolves selector aliases before rendering. It supports both static mappings and dynamic RegExp-based patterns, with recursive resolution.

How It Works

  1. Selector definitions are collected from config.selectors.selectors during rawConfigConfigured.
  2. During configureEngine, each definition is resolved and registered:
    • Static rules are stored for exact-match lookup.
    • Dynamic rules are stored for RegExp-based matching.
    • Autocomplete entries are added for known selectors.
  3. During transformSelectors, each selector string is recursively resolved through the SelectorResolver.
  4. If a selector does not match any rule, the original string is returned unchanged.
  5. Dynamic resolutions are automatically fed back into autocomplete via onResolved.

Config

ts
interface SelectorsConfig {
  /** Array of selector definitions. */
  selectors: Selector[]
}

Selector Definition Formats

There are four ways to define a selector.

String Form

A plain string is registered as an autocomplete suggestion only — no resolution rule is created.

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

// String form — autocomplete suggestion only, no resolution rule
const selector = defineSelector('hover')

Tuple Form — Static

A two-element tuple maps a selector name to one or more replacement strings. Use $ as a placeholder for the element's default selector (see The $ Placeholder below).

ts
type TupleFormStatic = [selector: string, value: string | string[]]
ts
import { defineSelector } from '@pikacss/core'

// Static tuple: [name, replacement]
// Use $ as a placeholder for the element's default selector
const hover = defineSelector(['hover', '$:hover'])
const focus = defineSelector(['focus', '$:focus'])
const firstChild = defineSelector(['first-child', '$:first-child'])

// Ancestor / wrapper selectors
const dark = defineSelector(['dark', '[data-theme="dark"] $'])

// At-rules — do NOT include $ inside at-rules
const md = defineSelector(['md', '@media (min-width: 768px)'])
const lg = defineSelector(['lg', '@media (min-width: 1024px)'])

// Multiple values (array form)
const hoverOrFocus = defineSelector(['hover-or-focus', ['$:hover', '$:focus']])

Tuple Form — Dynamic

A tuple with a RegExp pattern and a resolver function. The function receives the RegExpMatchArray and returns one or more replacement strings. An optional third element provides autocomplete hints.

ts
type TupleFormDynamic = [selector: RegExp, value: (matched: RegExpMatchArray) => string | string[], autocomplete?: string | string[]]
ts
import { defineSelector } from '@pikacss/core'

// Dynamic tuple: [pattern, resolver, autocomplete?]
// The resolver function receives the RegExp match array
const screen = defineSelector([
	/^screen-(\d+)$/,
	m => `@media (min-width: ${m[1]}px)`,
	['screen-640', 'screen-768', 'screen-1024'], // autocomplete hints
])

Object Form

Equivalent to tuple forms but with named properties. Supports both static and dynamic variants:

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

// Object form — static
const hover = defineSelector({
	selector: 'hover',
	value: '$:hover',
})

// Object form — dynamic (with autocomplete)
const breakpoint = defineSelector({
	selector: /^bp-(\d+)$/,
	value: (m: RegExpMatchArray) => `@media (min-width: ${m[1]}px)`,
	autocomplete: ['bp-640', 'bp-768', 'bp-1024'],
})

Full Example

ts
// pika.config.ts
import { defineEngineConfig } from '@pikacss/core'

export default defineEngineConfig({
	selectors: {
		selectors: [
			// String form — autocomplete only
			'my-selector',

			// Static selectors
			['hover', '$:hover'],
			['focus', '$:focus'],
			['first-child', '$:first-child'],
			['dark', '[data-theme="dark"] $'],
			['md', '@media (min-width: 768px)'],
			['lg', '@media (min-width: 1024px)'],

			// Dynamic selectors
			[
				/^screen-(\d+)$/,
				m => `@media (min-width: ${m[1]}px)`,
				['screen-640', 'screen-768', 'screen-1024'],
			],

			// Object form
			{
				selector: 'active',
				value: '$:active',
			},
		],
	},
})

Usage with pika()

Use selector names as keys in style definitions. Any key that is not a CSS property is treated as a selector:

ts
// Use selector names as keys in style definitions
const className = pika({
	color: 'black',
	hover: {
		color: 'blue',
	},
	dark: {
		color: 'white',
	},
	md: {
		fontSize: '1.25rem',
	},
})

Generated CSS output:

css
.a { color: black; }
.b:hover { color: blue; }
[data-theme="dark"] .c { color: white; }
@media (min-width: 768px) {
  .d { font-size: 1.25rem; }
}

The $ Placeholder

In selector values, $ is replaced with the element's default selector (which defaults to .%, where % is the atomic style ID placeholder). This enables pseudo-classes, ancestor selectors, and more.

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

// $ is replaced with the element's default selector (e.g., .%)
// Then % is replaced with the atomic style ID (e.g., a)

// Pseudo-class: append to the element selector
const hover = defineSelector(['hover', '$:hover'])
//   $ → .%  →  .%:hover  →  .a:hover

// Pseudo-element: append to the element selector
const before = defineSelector(['before', '$::before'])
//   $ → .%  →  .%::before  →  .a::before

// Ancestor selector: place $ after the parent
const dark = defineSelector(['dark', '[data-theme="dark"] $'])
//   $ → .%  →  [data-theme="dark"] .%  →  [data-theme="dark"] .a

// At-rules: no $ needed — the engine auto-appends the element selector
const md = defineSelector(['md', '@media (min-width: 768px)'])
//   no $, no %  →  defaultSelector (.%) is appended  →  two-level nesting:
//   @media (min-width: 768px) { .a { ... } }

Placeholder Behavior Summary

Definition$ → defaultSelectorFinal CSS
['hover', '$:hover'].%:hover.a:hover { ... }
['before', '$::before'].%::before.a::before { ... }
['dark', '[data-theme="dark"] $'][data-theme="dark"] .%[data-theme="dark"] .a { ... }
['md', '@media (min-width: 768px)'](no $)@media (min-width: 768px) { .a { ... } }

At-Rules

For CSS at-rules like @media or @container, do not include $ in the value. When the resolved selector does not contain the % placeholder, the engine automatically appends the default selector (.%) as a nested level, producing the correct two-level structure:

css
@media (min-width: 768px) {
  .a { ... }
}

WARNING

Do not embed $ inside an at-rule string (e.g., @media (...) { $ }). This produces @media (...) { .a } as a single block selector, resulting in invalid CSS.

Recursive Resolution

Selector resolution is recursive. A selector value can reference another selector name, and it will be resolved through the chain:

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

export default defineEngineConfig({
	selectors: {
		selectors: [
			// Base selector
			['hover', '$:hover'],
			// Alias that resolves to another selector
			['alias-hover', 'hover'],
			// Chained: group-hover resolves through its own rule
			['group-hover', '.group:hover $'],
		],
	},
})
// Using 'alias-hover' in a style definition:
// → resolves 'alias-hover' to 'hover'
// → resolves 'hover' to '$:hover'
// → final output: .a:hover { ... }

defineSelector Helper

Use the defineSelector() helper for type-safe selector definitions with full autocomplete:

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

// defineSelector() provides type safety and autocomplete
// for all selector definition formats
const hover = defineSelector(['hover', '$:hover'])
const dark = defineSelector({
	selector: 'dark',
	value: '[data-theme="dark"] $',
})

Engine API

Plugins can manage selectors programmatically:

  • engine.selectors.resolver — the SelectorResolver instance
  • engine.selectors.add(...list) — add selector definitions at runtime

Behavior Notes

  • Invalid selector config shapes are silently skipped.
  • Dynamic resolutions are cached after first resolution.
  • Both static and dynamic rules are stored in the SelectorResolver which extends AbstractResolver.
  • The $ placeholder respects the engine's defaultSelector option (defaults to .%).

Source Reference

  • packages/core/src/internal/plugins/selectors.ts

Next