Skip to Content
No-Code EditorCustom CSS

Custom CSS

Custom CSS is a power feature that lets you write raw CSS for any component. It supports Liquid templates, pseudo-selectors, media queries, keyframe animations, and CSS custom properties — giving you full control over styling that goes beyond the property panel.

How It Works

Every component is rendered with a class name funnel-cmp-{componentId}. When you write custom CSS, it is processed in two ways:

  1. Simple properties (e.g., color: red;) are applied as inline styles directly on the element.
  2. Complex rules (selectors, media queries, keyframes) are injected into a <style> tag with the component’s class as a scope.

This means your CSS is automatically scoped — it only affects the component it belongs to. You never need to write the class name yourself; the & selector refers to the component.

Basic Usage

Write CSS properties directly — no selector needed for simple overrides:

background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 16px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);

Liquid Templates in CSS

Any {{ }} or {% %} Liquid expression is evaluated before the CSS is applied. This makes your styles reactive to variable values.

Dynamic width from progress

width: {{ page.progressPercentage }}%;

The page’s progress percentage updates as the user navigates, and the width animates (if you add a transition property) accordingly.

Conditional color

{% if answers.goal == 'profit' %} color: #22c55e; background: rgba(34, 197, 94, 0.1); {% else %} color: #3b82f6; background: rgba(59, 130, 246, 0.1); {% endif %}

Product price in content

&::after { content: '{{ products.selected.price }}/{{ products.selected.period }}'; font-size: 14px; color: #666; }

Computed values

width: {{ page.current | divided_by: page.total | times: 100 }}%;

CSS with Liquid templates ({{ }} or {% %}) is re-evaluated whenever the referenced variables change. Static CSS (no Liquid) is computed once and cached for better performance.

Pseudo-Selectors

Use & to reference the component, then add pseudo-selectors:

&:hover { transform: translateY(-2px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15); } &:active { transform: translateY(0); } &::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: linear-gradient(to bottom, transparent, rgba(0,0,0,0.6)); border-radius: inherit; pointer-events: none; }

If you omit &, the selector is automatically scoped as a descendant of the component (.funnel-cmp-{id} your-selector). Use & explicitly for pseudo-selectors and direct component targeting.

CSS Custom Properties

Reference color variables defined in your funnel settings:

color: var(--color-primary); background: var(--color-surface); border: 1px solid var(--color-border);

Color variables are defined in funnel settings and injected as :root CSS custom properties with the pattern --color-{name}.

You can also define your own custom properties in CSS for local use:

--card-padding: 20px; padding: var(--card-padding);

Media Queries

Write responsive styles that adapt to screen width:

@media (min-width: 768px) { font-size: 24px; padding: 32px; } @media (prefers-color-scheme: dark) { background: #1a1a2e; color: #e0e0e0; }

Media query contents are automatically scoped to the component — simple properties inside a media query are wrapped in the component’s class selector.

Keyframe Animations

Define and use keyframe animations. Keyframe names are automatically scoped to prevent collisions between components:

@keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } animation: shimmer 2s ease-in-out infinite; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%;

The animation property references the keyframe name as you wrote it — the scoping is handled automatically. A keyframe named shimmer on component abc123 becomes shimmer-funnel-cmp-abc123 in the rendered CSS.

Combining Liquid with Complex CSS

Liquid and CSS features compose freely. Here is a conditional keyframe animation:

{% if data.showPulse %} @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } animation: pulse 2s ease-in-out infinite; {% endif %}

And a hover effect that changes based on a variable:

&:hover { background: {% if answers.plan == 'premium' %}#f59e0b{% else %}#3b82f6{% endif %}; transform: scale(1.02); }

Liquid is evaluated before CSS parsing. If a variable is undefined, it renders as an empty string — which may produce invalid CSS like width: %;. Use Liquid’s default filter to provide fallbacks: width: {{ page.progressPercentage | default: 0 }}%;.

Examples

Animated gradient border

@keyframes gradient-rotate { 0% { --angle: 0deg; } 100% { --angle: 360deg; } } border: 2px solid transparent; background: linear-gradient(white, white) padding-box, linear-gradient(var(--angle, 0deg), #667eea, #764ba2) border-box; animation: gradient-rotate 3s linear infinite;

Progress bar with Liquid width

background: var(--color-primary); height: 4px; border-radius: 2px; width: {{ page.progressPercentage | default: 0 }}%; transition: width 0.3s ease;

Conditional visibility via Liquid

{% if data.showBanner == true %} display: flex; opacity: 1; {% else %} display: none; {% endif %}

For simple conditional visibility, prefer Dynamic Properties on the hidden property instead of CSS. Dynamic Properties integrate with the component tree and are easier to manage.

Glass morphism card

background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 16px; &:hover { background: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.3); }
Last updated on