Problem
A common ask is to conditionally inject a small symbol or glyph into a node's display — e.g. append a "⚠️" after a value when some condition holds (validation error, a flagged field, a "modified since save" marker, etc.). The instinctive approach is a ::before/::after pseudo-element with a content string, toggled from a style function.
That doesn't work directly: theme/style functions return React.CSSProperties (src/types.ts:677) and the result is applied straight to style={...} (ValueNodeWrapper.tsx:548, CollectionNode.tsx:393). Inline styles cannot express pseudo-elements, so there's no inline-only way to add content: "⚠️".
This issue collects the viable approaches so we can pick one (or document a recommended recipe, and/or ship a preset/helper). It generalises the narrower validationStyles ⚠️-indicator question raised in #357.
For reference, the stable class names on value text are .jer-value-string, .jer-value-number, .jer-value-boolean, .jer-value-null (set in ValueNodes.tsx).
Potential implementations
1. Custom node component wrapping originalNode (recommended)
Custom components receive the fully-rendered built-in node as originalNode (and the key as originalNodeKey) when the definition opts in with passOriginalNode: true (src/types.ts:544, src/types.ts:585). So the component is a tiny wrapper that re-emits the original rendering and adds the glyph:
const FlaggedNode = ({ originalNode, nodeData }: CustomComponentProps) => (
<span>
{originalNode}
{isFlagged(nodeData) && <span> ⚠️</span>}
</span>
)
const flagDefinition: CustomNodeDefinition = {
condition: (nd) => typeof nd.value !== 'object', // which nodes are eligible
component: FlaggedNode,
passOriginalNode: true,
}
Because originalNode is the standard rendering, the wrapper inherits its theme styles, value-type formatting, and edit affordances — it adds nothing but the glyph, and works for every value type (and keys, via originalNodeKey) rather than being string-specific. Real DOM means tooltips / click handlers / arbitrary layout are all available.
Notes:
passOriginalNode is opt-in (default false) because it makes the editor build the original node's JSX up front — wasted work for nodes that fully replace rendering, but exactly what a wrap-and-augment node wants (src/types.ts:582).
- Most flexible, and the lowest-code option for a real text glyph.
2. CSS-variable bridge — real ::after, driven from a style function
React inline styles pass CSS custom properties through (and current @types/react types them), so a style function can toggle a variable that a real stylesheet rule consumes in a pseudo-element:
// style function (theme):
string: (nd) => (isFlagged(nd) ? { '--jer-flag': '"⚠️"' } : null)
/* one rule the consumer ships once: */
.jer-value-string::after { content: var(--jer-flag, ''); margin-left: 0.3em; }
The condition lives in the (inline) style function; the pseudo-element lives in static CSS. Keeps everything in the theme channel with no customNodeDefinitions.
Notes:
- Requires the consumer to ship that one stylesheet rule (could be packaged as a preset CSS file).
- CSS custom properties inherit — reset on descendants if that bleeds.
3. data-URI SVG background-image — self-contained, no stylesheet
Stays entirely inside the inline style object, so it needs no consumer CSS:
string: (nd) =>
isFlagged(nd)
? {
backgroundImage: 'url("data:image/svg+xml,…⚠️…")',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'right center',
paddingRight: '1.2em',
}
: null
Notes:
- Fully self-contained → good for a shippable theme preset.
- It's an image, not a text glyph; positioning is fiddly and emoji must be embedded as SVG
<text>.
Cross-cutting caveat (staleness)
Whichever channel is used, the condition is a render-time function of NodeData. If the symbol depends on cross-branch data, it only re-evaluates when that node re-renders — the same staleness documented in #357. The piercing channel is per-prop: theme identity for styles (Options 2/3), or memoizing the customNodeDefinitions array (Option 1). See #357 for the useStableValue-based discipline.
Suggested next step
Pick a recommended recipe to document (Option 1 reads as the default for a real glyph; Option 2 for staying theme-only; Option 3 for a zero-stylesheet preset), and decide whether any of these warrants a packaged helper/preset in @json-edit-react/utils or @json-edit-react/components.
Related: #357, #197.
Problem
A common ask is to conditionally inject a small symbol or glyph into a node's display — e.g. append a "⚠️ " after a value when some condition holds (validation error, a flagged field, a "modified since save" marker, etc.). The instinctive approach is a
::before/::afterpseudo-element with acontentstring, toggled from a style function.That doesn't work directly: theme/style functions return
React.CSSProperties(src/types.ts:677) and the result is applied straight tostyle={...}(ValueNodeWrapper.tsx:548, CollectionNode.tsx:393). Inline styles cannot express pseudo-elements, so there's no inline-only way to addcontent: "⚠️".This issue collects the viable approaches so we can pick one (or document a recommended recipe, and/or ship a preset/helper). It generalises the narrower⚠️ -indicator question raised in #357.
validationStylesFor reference, the stable class names on value text are
.jer-value-string,.jer-value-number,.jer-value-boolean,.jer-value-null(set in ValueNodes.tsx).Potential implementations
1. Custom node component wrapping
originalNode(recommended)Custom components receive the fully-rendered built-in node as
originalNode(and the key asoriginalNodeKey) when the definition opts in withpassOriginalNode: true(src/types.ts:544, src/types.ts:585). So the component is a tiny wrapper that re-emits the original rendering and adds the glyph:Because
originalNodeis the standard rendering, the wrapper inherits its theme styles, value-type formatting, and edit affordances — it adds nothing but the glyph, and works for every value type (and keys, viaoriginalNodeKey) rather than being string-specific. Real DOM means tooltips / click handlers / arbitrary layout are all available.Notes:
passOriginalNodeis opt-in (defaultfalse) because it makes the editor build the original node's JSX up front — wasted work for nodes that fully replace rendering, but exactly what a wrap-and-augment node wants (src/types.ts:582).2. CSS-variable bridge — real
::after, driven from a style functionReact inline styles pass CSS custom properties through (and current
@types/reacttypes them), so a style function can toggle a variable that a real stylesheet rule consumes in a pseudo-element:The condition lives in the (inline) style function; the pseudo-element lives in static CSS. Keeps everything in the theme channel with no
customNodeDefinitions.Notes:
3. data-URI SVG
background-image— self-contained, no stylesheetStays entirely inside the inline style object, so it needs no consumer CSS:
Notes:
<text>.Cross-cutting caveat (staleness)
Whichever channel is used, the condition is a render-time function of
NodeData. If the symbol depends on cross-branch data, it only re-evaluates when that node re-renders — the same staleness documented in #357. The piercing channel is per-prop: theme identity for styles (Options 2/3), or memoizing thecustomNodeDefinitionsarray (Option 1). See #357 for theuseStableValue-based discipline.Suggested next step
Pick a recommended recipe to document (Option 1 reads as the default for a real glyph; Option 2 for staying theme-only; Option 3 for a zero-stylesheet preset), and decide whether any of these warrants a packaged helper/preset in
@json-edit-react/utilsor@json-edit-react/components.Related: #357, #197.