css.marker / css.extended
API reference for css.marker and css.extended - Context-aware styling without DOM structure dependency
export type marker = (id: string, pseudo: string) => Marker;
export const marker: marker;
export type extended = <I extends string, P extends string>(id: I, pseudo: P) => Extended<I, P>;
export const extended: extended;The marker and extended APIs enable context-aware styling without relying on rigid DOM hierarchies (such as strict parent-child nesting) or combinator syntax. These paired APIs maintain atomicity while allowing child elements to respond to parent states.
Production Ready - These APIs compile to CSS Container Style Queries (@container style()). Since Firefox 151, style queries are fully supported across all major modern browsers (Chrome/Edge, Safari, and Firefox), making marker and extended completely stable for production use.
marker(id, pseudo)- Sets a CSS variable marker on a parent elementextended(id, pseudo)- Applies styles to children when the marker is active
Parameters
The parameters must be linked (match) in both cases.
id- A unique identifier linking the marker and extended stylespseudo- The pseudo-class state (e.g.,':hover',':focus',':active')
Example
import * as css from '@plumeria/core';
const animation = css.keyframes({
'0%': {
scale: 1.2,
},
'50%': {
scale: 1.3,
},
'100%': {
scale: 1.2,
},
});
const styles = css.create({
grand: {
display: 'flex',
flexDirection: 'column',
gap: 15,
alignItems: 'center',
justifyContent: 'center',
width: '100%',
minHeight: 250,
padding: 20,
border: '2px dashed #c4c4c466',
borderRadius: 12,
transition: 'all 0.3s',
...css.marker('grand', ':hover'),
},
parent: {
display: 'flex',
flexDirection: 'column',
gap: 10,
alignItems: 'center',
justifyContent: 'center',
width: '80%',
minHeight: 120,
padding: 10,
backgroundColor: '#f5f5f7',
border: '2px solid #c4c4c444',
borderRadius: 8,
transition: 'all 0.3s',
...css.marker('parent', ':hover'),
},
child: {
padding: '8px 16px',
fontWeight: 'bold',
color: '#86868b',
background: '#e8e8ed',
borderRadius: 6,
transition: 'all 0.3s',
[css.extended('grand', ':hover')]: {
color: '#0066cc',
background: '#e1f0ff',
},
[css.extended('parent', ':hover')]: {
color: '#ffffff',
background: '#ff9500',
scale: 1.2,
':hover': {
background: 'skyblue',
animationName: animation,
animationDuration: '1.2s',
animationTimingFunction: 'ease-in-out',
animationIterationCount: 'infinite',
},
},
},
discription: {
fontSize: '14px',
color: '#86868b',
},
});
const MarkerExtended = () => {
return (
<div classStyle={styles.grand}>
<span classStyle={styles.discription}>Grand Container (Hover me)</span>
<div classStyle={styles.parent}>
<span classStyle={styles.discription}>Parent Container (Hover me too)</span>
<div classStyle={styles.child}>Child Element</div>
</div>
</div>
);
};.xtkxh191:hover {
--xl0a2l54-grand-hover: 1;
}
.xhcsnlrf:hover {
--xmlppxj5-parent-hover: 1;
}
@container style(--xl0a2l54-grand-hover: 1) {
.x84muc4x:not(#\#) {
color: #06c;
}
.x2x6k4j2:not(#\#):not(#\#) {
background-color: #e1f0ff;
}
}
@container style(--xmlppxj5-parent-hover: 1) {
.xr8ck8y9:not(#\#) {
color: #fff;
}
.xb1yp2w2:not(#\#):not(#\#) {
background-color: #ff9500;
}
.xewf73ay:not(#\#) {
scale: 1.3;
}
.x8odz1r5:not(#\#):not(#\#) {
animation-name: kf-xmortmuw;
}
.x0guwijo:not(#\#):not(#\#) {
animation-duration: 1.2s;
}
.xek6euru:not(#\#):not(#\#) {
animation-timing-function: ease-in-out;
}
.xwdc6dqv:not(#\#):not(#\#) {
animation-iteration-count: infinite;
}
.xd9yj17o:not(#\#):hover {
color: #0ff;
}
}How it works
Internal
marker() generates a CSS variable when the pseudo-class is active:
.parent:hover {
--xyuz5mma-ul-hover: 1;
}extended() wraps child styles in a container query:
@container style(--xyuz5mma-ul-hover: 1) {
.child { /* styles */ }
}The CSS variable acts as a signal that propagates to descendants without DOM structure dependency.
ESLint Rule
The no-combinator rule enforces this pattern by disallowing combinator syntax.
Functional pseudo-classes
Any pseudo-class is accepted, including the functional ones.
const styles = css.create({
wrap: {
...css.marker('wrap', ':has(a:hover)'),
},
child: {
[css.extended('wrap', ':has(a:hover)')]: { color: 'red' },
},
});.xwpr9f08:has(a:hover) {
--xg204awn-wrap-has: 1;
}
@container style(--xg204awn-wrap-has: 1) {
.xof70rr5:not(#\#) {
color: red;
}
}:has() reverses the direction the marker travels: the state belongs to a
descendant, the marker lands on the element that contains it, and the styles land
on any descendant of that element. CSS spells the same relation as
.a:has(.b:hover) .c, naming the path between all three; the marker names none
of it.
:defined matches every standard element, so it turns the marker into a
permanent signal — the plain descendant relation, with no state involved.
The marker has to be above it
An extended style applies to elements below the marked one, and to nothing
else. Render the same component outside that subtree and the style is simply
absent — no error, no warning, because the query has nothing to match. With a
state like :hover this reads as "the state never fired"; with :defined,
which is always on, it reads as the style having disappeared.
Every combinator, expressed
| CSS | Plumeria |
|---|---|
.a .b — descendant, on a state | css.marker(id, ':hover') + css.extended |
.a .b — descendant, no state | css.marker(id, ':defined') + css.extended |
.a > .b — child | css.marker(id, ':defined') + css.extended on the child |
.a + .b, .a ~ .b — sibling | css.marker(id, ':defined') + css.extended on the sibling |
.a:hover ~ .b — sibling reacting to a state | css.marker(id, ':has(a:hover)') + css.extended |
.a:has(.b) .c — a descendant's state, back up and down again | css.marker(id, ':has(input:focus)') + css.extended |
The marker goes on the element both sides share — for a child that is the parent,
for a sibling their common parent. What separates a child from a sibling is not a
selector but which element you hand extended to.
That last part is why migrate --from css-modules
writes these pairings for you everywhere except the sibling, which it reports: a
style query reaches descendants, never siblings, so the relation rests on a choice
of element rather than on the selector it read, and only you can make it.
The CSS column names a path; the Plumeria column does not. marker and
extended are linked by an id, so the styles reach every descendant at any
depth, and moving an element deeper does not invalidate the pair.
Reach for marker / extended first. The alternative is a selector key —
a combinator inside a functional pseudo-class, which no-combinator permits,
anchored on something you write yourself:
const styles = css.create({
cell: { ':is([data-row] > *)': { color: 'red' } },
});That names the path, so moving the element breaks it, and it repeats the whole
selector for every declaration. The marker pair names no path, and every
extended sharing an id lands in one @container block.
| Targets under one relation | Selector key | marker / extended |
|---|---|---|
| 1 | 94 B | 147 B |
| 5 | 281 B | 43 B |
The selector key is the escape hatch, for when the target is markup you do not write and therefore cannot attach a style to.
What the container query costs
extended compiles to a style query, which the engine re-evaluates per container
when a state changes. Style recalculation, measured over a class toggle in
headless Chromium, median of 30 runs:
| Elements | Plain CSS | Selector key | marker / extended |
|---|---|---|---|
| 500 | 0.009 ms | 0.006 ms | 0.012 ms |
| 2,000 | 0.008 ms | 0.007 ms | 0.012 ms |
| 8,000 | 0.005 ms | 0.005 ms | 0.012 ms |
The cost does not grow with the page. Toggling every anchor at once is the case where it does: 8,000 simultaneous toggles cost 26 ms against 7 ms for a selector key. That is not a shape user interfaces take, but it is where the difference lives if you go looking for it.