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
- Selector definitions are collected from
config.selectors.selectorsduringrawConfigConfigured. - 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.
- During
transformSelectors, each selector string is recursively resolved through theSelectorResolver. - If a selector does not match any rule, the original string is returned unchanged.
- Dynamic resolutions are automatically fed back into autocomplete via
onResolved.
Config
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.
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).
type TupleFormStatic = [selector: string, value: string | string[]]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.
type TupleFormDynamic = [selector: RegExp, value: (matched: RegExpMatchArray) => string | string[], autocomplete?: string | string[]]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:
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
// 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:
// 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:
.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.
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 | $ → defaultSelector | Final 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:
@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:
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:
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— theSelectorResolverinstanceengine.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
SelectorResolverwhich extendsAbstractResolver. - The
$placeholder respects the engine'sdefaultSelectoroption (defaults to.%).
Source Reference
packages/core/src/internal/plugins/selectors.ts
Next
- Continue to Shortcuts