Skip to content

Conversation Block Styling Feasibility Report

Focused analysis of hardcoded styling in the conversation rendering layer. For broader redesign context, see todos/ConversationRedesign/feasibility.md and conversation-v2-plan.md.


1. Hardcoded Styling Inventory

1.1 CreativeConversationBlock.vue

The outer container is mostly configurable. Its styles() computed pulls sizing, border, box-shadow, background, alignment, and rotation from conversationProperties. The hardcoded parts:

PropertyValueConfigurable?
display: flex (wrap)flexNo -- structural
position: absolute (wrap + main)absoluteNo -- structural
pointerEvents: none (wrap) / auto (main)none/autoNo -- functional
overflow: hidden (main block)hiddenNo -- clips content
overflowY: auto, overflow: hidden scroll (scroll viewport)scrollNo -- enables scrolling
scrollbarWidth: none + ::-webkit-scrollbar: display: nonehidden scrollbarCosmetic but intentional
maxHeight: 100% (scroll viewport)100%No -- constrains scroll
flexDirection: column (scroll viewport)columnNo -- stacks messages
maskImage (scroll viewport, when scrollOut = fadeOut)gradientPartially -- scrollOut toggle exists but gradient values are hardcoded
direction: inherit (scroll content)inheritNo -- structural
boxSizing: border-box !important (scroll content)border-boxNo -- structural
minHeight (scroll content)Dynamic from DimensionManagerNo -- functional (prevents choices from being cut off)

Verdict: The container layer is well-structured. Almost everything visual comes from block properties. The scroll viewport has functional CSS that must stay.

1.2 MessageHolder.vue (the big one)

This is where most hardcoded styling lives. Every value below is baked into the styles() computed with no corresponding block property.

Message wrap:

PropertyValueCategory
marginBottom0pxCosmetic
opacity0 (initial)Functional -- entrance animation
transformtranslate3d(0, 10px, 0) (initial)Functional -- entrance animation
fontSize0pxCosmetic (prevents whitespace between inline-blocks)
marginLeft / marginRight15pxCosmetic
&:first-child paddingTop15pxCosmetic
&.not-first-in-block marginTop3pxCosmetic
&.not-first-in-block paddingLeft (when icon)36pxCosmetic (but derived from avatar 30px + 6px margin)
&.visible opacity1Functional -- entrance animation
&.visible transformtranslate3d(0, 0, 0)Functional -- entrance animation
&.inputneeded marginTop6pxCosmetic
&.inputentered marginBottom6pxCosmetic
&.blocks-before-rich marginTop10px !importantCosmetic
&.blocks-after-rich marginBottom10px !importantCosmetic

Avatar:

PropertyValueCategory
width / height30pxCosmetic
marginRight6pxCosmetic
borderRadius50%Cosmetic
displayinline-block or noneFunctional (show/hide)
backgroundurl(...) center center / cover no-repeatPartially configurable (URL from senderIcon)

Message bubble:

PropertyValueCategory
padding8px 12pxCosmetic
borderRadius1px [configurable] [configurable]Mixed -- top-left 1px is hardcoded for "tail" effect
transition opacity200ms ease-out (when typing animation enabled)Functional
opacity (bubble)0 initially, 1 after typing delayFunctional
userSelectnoneFunctional
verticalAligntopCosmetic
positionrelativeStructural
&.inputentered borderRadius[configurable] [configurable] 1px -- top-right 1px for response tailMixed
&.inputentered transitionnoneFunctional

Alignment classes (choice/message middle/left/right):

PropertyValueCategory
display: flex + justifyContent variationscenter, flex-end, flex-startPartially configurable via alignment property, but flex layout is hardcoded

Summary: ~20 hardcoded cosmetic values, ~8 functional values, ~4 mixed.

1.3 Choice.vue

PropertyValueCategory
floatrightCosmetic (legacy, should be flex)
padding6px 12pxCosmetic
marginBottom / marginLeft / marginRight6px eachCosmetic
outline / background / bordernoneReset (then overridden by choiceProperties)
fontWeightnormalCosmetic (overridden by choiceProperties font)
borderRadiusfrom choiceProperties or 15px fallbackConfigurable (with hardcoded fallback)
borderTopRightRadiusfrom choiceProperties or 1px fallbackMixed -- 1px tail effect is hardcoded
transitionall 300ms ease 0s, width 0sCosmetic
positionrelativeStructural
pointerEventsallFunctional
cursorpointerFunctional
&:hover transformscale3d(1.05, 1.05, 1)Cosmetic
&:hover filterbrightness(1.1) saturate(1.05)Cosmetic
&:active transformscale3d(1.03, 1.03, 1)Cosmetic
&:active filterbrightness(1.08) saturate(1.05)Cosmetic
whiteSpacepre-lineSemi-functional (preserves line breaks)
wordBreakbreak-wordSemi-functional (prevents overflow)
@keyframes buttonAppearingopacity 0->1, top 20px->0Functional (entrance)
animationDelayindex * 120 + 1msCosmetic (stagger timing)
animationDuration300msCosmetic
Container flexDirectionrow or column based on fitConfigurable via fit property

Summary: ~14 cosmetic values, ~4 functional, ~3 semi-functional.

1.4 Atoms (CloseButton, Tagline, ExpandableIcon, RemoveButton)

These are peripheral to the conversation flow itself. Their styling is mostly configurable via their respective block properties (closeProperties, taglineProperties, iconProperties, removeProperties). Hardcoded values are mainly structural positioning (position: absolute, transform: translateY(-50%)) and the X-icon diagonal rendering. Not relevant for a conversation CSS reset.

1.5 CreativeBody.vue

Container styling. Hardcoded: position: absolute, transition: all 0.3s, borderRadius: 0 0 5px 5px, backgroundSize: cover. The 5px border-radius is cosmetic but applies to the entire creative body, not conversation-specific. Not a conversation reset concern.


2. Functional vs Cosmetic Classification

Functional (must keep)

These properties are required for scroll behavior, flow navigation, entrance animations, or user interaction. Removing them breaks the conversation.

SystemPropertiesWhy
Scroll containeroverflow: hidden scroll, maxHeight: 100%, flexDirection: columnEnables vertical scrolling, constrains height
Auto-scrollscrollTop manipulation via requestAnimationFrameSmooth scroll-to-bottom after new messages
Entrance animationopacity: 0 -> 1, transform: translate3d(0, 10px, 0) -> (0,0,0)New message appearance, triggers scroll timing
Typing delaytransition: opacity 200ms on bubbleSynchronized with conversationFlow.ts timing
Button stagger@keyframes buttonAppearing, animationDelay: index * 120msSequential button entrance
Fade-out maskmaskImage: linear-gradient(...) tied to scrollTopScroll position indicator
Scroll content minHeightDynamic from DimensionManagerPrevents choices from being clipped at bottom
Pointer eventsnone on wrap, auto on interactive elementsClick-through to background
Input switchingv-show / class toggles on inputEnteredChoice-to-response transition

Cosmetic (safe to make configurable)

These properties control appearance only. Changing them affects how things look but not how they behave.

ElementPropertiesCurrent values
Message wrap marginsmarginLeft, marginRight15px
Message wrap first-child paddingpaddingTop15px
Inter-message gapmarginTop (not-first-in-block)3px
Input-needed gapmarginTop (inputneeded)6px
Input-entered gapmarginBottom (inputentered)6px
Avatar sizewidth, height30px
Avatar marginmarginRight6px
Avatar shapeborderRadius50%
Bubble paddingpadding8px 12px
Bubble border-radiusborderRadius15px (with 1px tail)
Choice button paddingpadding6px 12px
Choice button gapmargin6px
Choice hover/active effectstransform, filterscale + brightness
Choice stagger timinganimationDelay, animationDuration120ms * index, 300ms
Choice entrance distancetop in keyframe20px
Hidden scrollbarscrollbarWidth: nonenone

Gray area

PropertyIssue
whiteSpace: pre-linePreserves line breaks in message text. Removing could collapse multi-line messages.
wordBreak: break-wordPrevents long words from overflowing bubbles. Removing could break layout.
fontSize: 0px on message-wrapPrevents whitespace between inline-block children. Removing adds unwanted gaps.
Avatar paddingLeft offset (36px)Derived from avatar size (30) + margin (6). If avatar size becomes configurable, this must recalculate.
Bubble 1px border-radius (tail)Design choice baked in. Some users want uniform radius, others want the tail.

3. CSS Reset Feasibility

3.1 How existing resets work

The engine has a three-layer reset system:

  1. globalReset.ts -- always applied. Resets common HTML elements, sets baseline box-sizing, font smoothing.
  2. bannerReset.ts -- applied when creativeType === BANNER. Heavy reset of ~65 HTML elements.
  3. expandableReset.ts -- applied when creativeType === EXPANDABLE. Resets maxWidth: none on all elements.

Resets are injected as <style> elements via StyleComposer.applyStyleObject(), tracked by identifier (e.g., data-banner-reset). They apply BEFORE component styles, so component styles() computed values override them.

3.2 A conversationReset approach

A CSS reset for conversation elements would follow the same pattern:

ts
// src/style-engine/conversationReset.ts
export const conversationReset = {
  // Reset message wrapper cosmetic styling
  '.message-wrap': {
    margin: '0',
    padding: '0',
    fontSize: 'inherit',
  },

  // Reset bubble cosmetic styling
  '.message-bubble': {
    padding: '0',
    borderRadius: '0',
    background: 'none',
    border: 'none',
    boxShadow: 'none',
  },

  // Reset avatar cosmetic styling
  '.message-avatar': {
    width: 'auto',
    height: 'auto',
    margin: '0',
    borderRadius: '0',
  },

  // Reset choice cosmetic styling
  '.choices a, .choices button': {
    padding: '0',
    margin: '0',
    borderRadius: '0',
    background: 'none',
    float: 'none',
  },
}

Application: Via StyleEngine.init() gated on a resetConversationStyles flag on conversationProperties, or via a cleanSlate toggle (as described in the existing V2 plan).

3.3 Challenges

  1. Class name obfuscation. The engine uses obfuscateClassNames() which transforms class names in production builds. A reset targeting .message-wrap won't work if the actual class is mw-a3f. The reset must use the same obfuscation pipeline, or target via attribute selectors ([data-step]) or parent-child combinators.

  2. Specificity wars. Component styles() computed values are injected per-instance with unique class selectors (e.g., .message-wrap-${uniq}). A global reset has lower specificity and would be overridden by the component styles. The reset would need to be applied AFTER component styles, or use !important, or the component must conditionally skip its cosmetic styles when reset is active.

  3. No clean separation in the current code. MessageHolder's styles() mixes functional and cosmetic properties in the same selector. You can't simply "turn off" the cosmetic ones without modifying the component. A reset would add competing styles rather than removing them.

  4. The 1px tail. The asymmetric borderRadius: 1px 15px 15px on message bubbles is deeply embedded in the bubble styling logic. A reset that zeros border-radius would remove this, but there's no way to make just the tail optional without touching the component.

3.4 Verdict: CSS Reset

Feasible but fragile. A conversation reset is technically possible using the existing StyleEngine pattern, but it would fight against the component's own styles() output rather than cleanly replacing it. The result would be two competing style layers -- the component's defaults and the reset's overrides -- creating maintenance burden and specificity issues.

Better approach: Instead of a CSS reset that fights the component, modify the component to conditionally skip cosmetic defaults. This is the cleanSlate toggle from the V2 plan -- a boolean that makes the styles() computed emit only functional CSS, leaving everything else to builder configuration.


4. V2 Feasibility -- Lightweight Conversation Component

4.1 What already exists

The todos/ConversationRedesign/conversation-v2-plan.md is a complete 15-task implementation plan for a V2 rendering path. It proposes:

  • MessageHolderV2.vue -- clean DOM with data-* attributes, all hardcoded values extracted to named constants, CSS custom properties for override
  • ChoiceV2.vue -- flex/grid layout (replacing float), configurable stagger, variant/outcome data attributes, feedback delay
  • FlowGroup.vue -- display: contents wrapper for grouping conversation steps
  • autoScrollV2.ts -- configurable scroll duration/easing
  • conversationVersion: 1 | 2 flag for backward compatibility
  • cleanSlate toggle for minimal default styling
  • CSS custom properties layer (--conv-message-margin-x, --conv-bubble-radius, etc.)

4.2 Scope of refactoring

What changesEffortRisk
New MessageHolderV2.vue (~350 lines)MediumLow -- isolated new file
New ChoiceV2.vue (~250 lines)MediumLow -- isolated new file
New FlowGroup.vue (~30 lines)LowLow -- simple wrapper
New autoScrollV2.ts (~60 lines)LowLow -- isolated utility
Modify CreativeConversationBlock templateLowLow -- conditional branch, V1 untouched
Extract constants to constants.tsLowNone -- additive
Add types to payload-v2/index.tsLowNone -- additive
Frontend: types, defaults, config sectionsMediumLow -- additive
Total new code~700 lines engine + ~200 lines frontend

4.3 What does NOT change

  • conversationFlow.ts (flow processor) -- reused as-is
  • flowModifier.ts (adds steps to liveFlow) -- reused as-is
  • FlowComponentInterface -- extended but backward-compatible
  • DataStore.liveFlow -- same reactive array
  • All existing creatives -- stay on V1 until explicitly switched

4.4 Biggest obstacles

  1. Pixel-perfect parity. V2 must render identically to V1 with default settings. Any visual difference in existing creatives would be a regression. The 20+ hardcoded values must be carefully matched in CONVERSATION_DEFAULTS.

  2. Custom CSS compatibility. Users with custom CSS targeting V1 class names (.message-wrap, .message-bubble, .choices a) will break if they switch to V2. The V2 DOM uses different selectors (data attributes, unique class names). Migration needs clear documentation.

  3. Timing coordination. The conversation flow has a tight timing chain: typing delay -> bubble opacity transition -> scroll-to-bottom. MessageHolderV2 must replicate this chain exactly or messages will appear before/after scrolling, creating jarring UX.

  4. Fill-container mode. MessageHolder uses <Teleport> to move components out of the scroll container when singleBlockFillsContainer is active. This is tricky to replicate because it affects both DOM structure and event handling.

  5. Per-button blockStyles. Choice buttons can have per-button style overrides from flow operators (ChangeText, ChangeImage). ChoiceV2 must apply these via the same getFlatOpBlockStyles() mechanism.

4.5 Verdict: V2

Feasible and well-planned. The existing plan is thorough and architecturally sound. The conversationVersion flag provides clean backward compatibility. The main risk is subtle timing/layout regressions that would only surface in complex flow configurations.

Recommended approach:

  1. Build V2 components
  2. Test against a diverse set of existing creatives (banner, expandable, different speeds, with/without icons, with custom CSS)
  3. Ship behind the version toggle -- users opt in per creative
  4. Once confident, make V2 the default for new creatives

5. Summary

QuestionAnswer
How much is hardcoded?~35 cosmetic values across MessageHolder + Choice. Container and atoms are mostly configurable.
What can be removed safely?All cosmetic values (margins, padding, border-radius, colors, hover effects, stagger timing). ~20 values in MessageHolder, ~14 in Choice.
What must stay?Scroll overflow, entrance opacity/transform, typing delay transition, pointer events, min-height, mask-image, flex-direction. ~12 functional values.
CSS reset feasibility?Technically possible but fragile due to specificity conflicts and class obfuscation. A cleanSlate toggle in the component is better.
V2 feasibility?High. Plan exists. ~900 lines of new code across 4 new files. Backward-compatible via version flag. Main risk: timing regressions.

Key Source Files

FileRepoHardcoded values
src/components/conversationflow/MessageHolder.vueCE20+ cosmetic, 8 functional
src/components/blocks/basic/Choice.vueCE14 cosmetic, 4 functional
src/components/creative/CreativeConversationBlock/CreativeConversationBlock.vueCE3 cosmetic, 9 functional
src/components/conversationflow/CreativeBody.vueCE3 cosmetic, 4 functional
src/utils/autoScroll.tsCE1 (SCROLL_DURATION)
src/style-engine/dimensionmanager/banner.tsCEDynamic minHeight calc
  • todos/ConversationRedesign/feasibility.md -- broader problem analysis, approach comparison
  • todos/ConversationRedesign/conversation-v2-plan.md -- 15-task implementation plan
  • todos/ConversationRedesign/flow-group-visual-companion.html -- interactive flow-group concept

Internal documentation