Skip to content

Engine Styling System

How Creative-Engine components apply CSS to rendered creatives.

Core pattern: CSS-in-JS via styles() computed

Every block component in Creative-Engine defines a styles() computed property that returns a JavaScript object. This object maps CSS selectors to style objects:

javascript
styles() {
  return {
    [this.toClassSelector(formBlock)]: {
      position: 'relative',
      display: 'flex',
      borderRadius: formProps.style.borderRadius,
      ...getBackgroundProperties(formProps),
      ...getBorderProperties({ border: formProps.border, borderStyle: formProps.borderStyle }),
    },

    '.some-child-element': {
      fontSize: '14px',
      color: '#000',
    },

    // Pseudo-classes
    [`${this.toClassSelector(inputField)}:hover`]: pseudoClassStyles.hover,

    // Pseudo-elements
    '.dropdown-input-container::after': {
      content: '""',
      position: 'absolute',
    },

    // @keyframes
    '@keyframes loading-dots': {
      '0%, 80%, 100%': { opacity: 0.3, transform: 'translateY(0)' },
      '40%': { opacity: 1, transform: 'translateY(-4px)' },
    },
  }
}

The engine's runtime processes this object and generates actual <style> tags in the creative's DOM. This keeps all styling in JavaScript and avoids static CSS files.

Why this matters: no <style> blocks in .vue files

CRITICAL: Engine components must NEVER use <style> or <style scoped> blocks. Vite extracts these into separate .css files during the production build. This breaks the build pipeline because:

  1. The injectTag.ts post-processor uses assets[0] to find the engine JS bundle
  2. A .css file (e.g. creative-engine-C9e3UedP.css) sorts alphabetically before creative-engine.js
  3. So assets[0] picks the CSS file instead of the JS
  4. The CSS content gets injected into stub.js as creativeScripts
  5. When loaded, CSS inside a <script> tag causes SyntaxError: Unexpected token '{'

The rule is simple: All styling goes through styles(). No exceptions.

Class name system

Block components get scoped class names via this.blockClassNames. These are unique per block instance:

javascript
const { formBlock, innerWrap, inputField, submitButton } = this.blockClassNames

Use this.toClassSelector(name) to convert a class name to a CSS selector (prefixes with .).

Use this.toMultiClassSelector(name1, name2, ':hover') for comma-separated selectors.

For non-scoped elements (children that don't need unique class names), use plain string selectors:

javascript
'.dropdown-input-container select': { ... }
'.checkbox-option-label': { ... }

Style helper functions

Common style properties are built using shared helpers imported from the engine:

HelperPurpose
getBackgroundProperties(block)Background color/gradient from block config
getBorderProperties({ border, borderStyle })Border width/style/color
getBoxShadowProperties({ boxShadow, boxShadowSettings })Box shadow
getPaddingProperties(style)Padding from style object
getMarginProperties(style)Margin from style object
getPositionAsPx(style)Top/right/bottom/left as px
getSizeProperties({ sizeStyle }, responsive)Width/height
getFlexAlign(alignment)Converts 'left'/'center'/'right' to flex values
getTransformRotation(degrees)CSS rotate transform
getTransitionString(transition)Transition shorthand
parseCustomStyling({ style, customCSS, customFont })Custom CSS overrides

State-based styling

Interactive elements (inputs, buttons) support state styles (hover, focus, active, disabled, etc.). These are configured per-state in the creative data and applied via:

  1. Real pseudo-classes for runtime: ${selector}:hover, ${selector}:focus
  2. State preview classes for builder: ${selector}.state-hover, ${selector}.state-focus

Both are defined in styles() and point to the same style object.

Animation styles

Animation styles from the block's animation config are integrated via:

  • this.animationStyleProps — applied to the block element
  • this.animationHoverStyle — applied to &:hover
  • this.animationKeyframeRule@keyframes definitions

These come from the AnimationMixin and are spread into the styles() return object.

Per-input custom styles

Individual form inputs can override global styles. These are computed in perInputCustomStyles and spread into styles():

javascript
return {
  ...this.perInputCustomStyles,
  ...this.visualElementsInsideFormStyles,
  ...this.animationKeyframeRule,
  // ... rest of styles
}

Internal documentation