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:
- Simple properties (e.g.,
color: red;) are applied as inline styles directly on the element. - 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);
}