Skip to content

CSS Classes from Flow Variables

Summary

Automatically inject CSS classes onto the creative widget based on flow variable state. Users can write conditional CSS targeting these classes — no JavaScript needed.


MVP — Implemented ✅

Classes added to widget root (.creative-outer-container)

Variable stateCSS classExample use
answer = "Oslo".cavai-answer-osloStyle based on what user chose
answer.position = 2.cavai-answer-position-2Style based on which option
score = 3.cavai-var-score-3Style based on custom variable

Class naming: cavai- prefix to avoid collisions. Values are slugified (lowercase, spaces/special chars → hyphens).

Important: Classes go on .creative-outer-container, NOT #creative-container. The latter has a reactive Vue :class binding that overwrites manually added classes on re-render. .creative-outer-container is set once in Creative.vue mounted() and is stable.

Files changed

CE/src/utils/flowClasses.ts (NEW)

  • slugify(str) — safe CSS class names
  • updateFlowClasses() — removes old cavai-* classes, adds current state
  • Targets .creative-outer-container with fallback to DataStore.mainDivID
  • Only touches cavai-answer-* and cavai-var-* classes — never removes existing classes

CE/src/style-engine/styleTreeParser/index.ts (MODIFIED)

CSS namespacing fix: .cavai-* selectors are combined with the container selector (.creative-outer-container.cavai-answer-oslo) instead of nested as descendants (.creative-outer-container .cavai-answer-oslo). This is because the classes are ON the container element itself, not on a descendant.

ts
// In prependOuterContainerSelectorConditionally:
if (subSelector.startsWith('.cavai-')) {
  return containerSelector + subSelector  // Combined: .creative-outer-container.cavai-answer-oslo
}
return [containerSelector, subSelector].join(' ')  // Default: .creative-outer-container .selector

Safe for legacy: no existing creatives use .cavai- prefixed CSS classes.

CE/src/components/blocks/functional/SetVariable.ts

Added updateFlowClasses() call after variable assignment.

CE/src/components/conversationflow/MessageHolder.vue

Added updateFlowClasses() call after answer recording in handleInputEntry.

Practical limitation discovered

Per-answer CSS classes (.cavai-answer-oslo) work technically — the class IS on the container — but background/outline on the container aren't visible because child elements have opaque backgrounds. Also, .choice elements become .response elements after the user clicks, so CSS targeting .choice doesn't work post-answer.

This is why Phase 2 pivoted to .cavai-answer-correct/.cavai-answer-wrong classes, which are more practical for the main quiz use case.

How to test

  1. Rebuild CE
  2. Create flow: Question → Answers → CSS operator
  3. CSS operator: .cavai-answer-correct { background: green !important; }
  4. Mark one answer as correct (✓ toggle), preview, click
  5. DevTools: inspect .creative-outer-container → should have cavai-answer-correct or cavai-answer-wrong class

Phase 2 — Correct/Wrong ✅ Implemented

What it does

Mark answer operators as "correct" in the builder (✓ toggle). When user picks the correct answer → .cavai-answer-correct on widget root. Wrong answer → .cavai-answer-wrong. Enables quiz styling with zero code.

Files changed

Builder: AnswerOp.vue

  • ✓ toggle button: position: absolute; top: 4px; right: 24px (next to gear icon)
  • toggleCorrect(): Vue.set(this.op.properties.body, 'isCorrectAnswer', !current)
  • Green circle when active, subtle grey when inactive
  • Undo/redo watcher added in OperatorBase.vue

Composer: remapData.ts

Passthrough at end:

typescript
if (body.isCorrectAnswer) {
  comp.payload.isCorrectAnswer = body.isCorrectAnswer
}

CE: MessageHolder.vue

In handleInputEntry, after recording the answer:

typescript
const hasCorrectMarker = this.componentsWithOverrides.some(c => c.payload?.isCorrectAnswer)
if (hasCorrectMarker) {
  const pickedCorrect = this.componentsWithOverrides[meta]?.payload?.isCorrectAnswer === true
  DataStore.flowVariables['_answerCorrect'] = pickedCorrect
}

CE: flowClasses.ts

typescript
if (vars._answerCorrect !== undefined) {
  root.classList.add(vars._answerCorrect ? 'cavai-answer-correct' : 'cavai-answer-wrong')
}

Example quiz CSS

css
.cavai-answer-correct { background: #e8f5e9; }
.cavai-answer-wrong   { background: #ffebee; }

Remaining work for Phase 2

  • [ ] answer.result variable: Add DataStore.flowVariables['answer.result'] = 'correct' | 'wrong' — a user-facing string for use in {answer.result} templates and "when answer.result = correct" conditions. Currently only _answerCorrect (boolean, internal) exists.
  • [ ] Refresh bug: _answerCorrect (and all flowVariables) persists across preview refresh. resetDataStore() replaces the reactive proxy reference but doesn't clear the old proxy in-place.

Phase 3 — More built-in classes (ideas)

ClassWhen addedUse case
.cavai-has-answeredAfter first answerShow/hide elements after interaction
.cavai-step-{n}On each flow stepProgressive styling per step
.cavai-answered-{n}-timesIncremented per answerUnlock content after N interactions
.cavai-has-{varname}When variable existsStyle based on variable presence
.cavai-var-{name}-high/medium/lowThreshold detectionScore ranges without exact values

Threshold classes

Instead of only .cavai-var-score-3 (exact value), add range classes:

score >= 10 → .cavai-var-score-high
score >= 5  → .cavai-var-score-medium
score >= 0  → .cavai-var-score-low

Thresholds could be configurable per variable in the SetVariable operator UI.


Phase 4 — Autocomplete (future)

CSS operator autocomplete

When typing .cavai- in the CSS operator, suggest available class names:

  • Infer from answer operators: .cavai-answer-oslo, .cavai-answer-bergen
  • Infer from SetVariable operators: .cavai-var-score-0, .cavai-var-status-active

JS operator autocomplete

Suggest document.querySelector('.cavai-...') patterns.


Out of Scope

  • CSS-in-JS reactive system (CSS classes are sufficient)
  • Server-side class generation
  • Class-based analytics

Internal documentation