Custom CSS
Every item has an Advanced (custom CSS) section at the bottom of Properties → Design. It lets you set any CSS property on the item directly - for the few looks that don't have a control yet.
Quick start
Click it on the canvas, or its row in the Layers panel.
Properties → Design, scroll to the bottom, open Advanced (custom CSS).
For example, a soft coloured glow around the text:
{ "textShadow": "0 0 24px rgba(99, 102, 241, 0.8)" }It applies when the box loses focus. If the JSON isn't valid, you'll see “That is not valid JSON, so nothing was changed” and the item stays as it was.
The format
The box takes one JSON object: property names on the left, values on the right.
| Rule | Example |
|---|---|
| Property names are camelCase (drop the dash, capitalise the next letter). | background-color → "backgroundColor" |
| Browser-prefixed properties start with a capital. | -webkit-text-stroke → "WebkitTextStroke" |
| Names and text values go in double quotes. | "mixBlendMode": "screen" |
| A plain number means pixels for sizes. | "borderRadius": 24 is 24px |
| …but has no unit for opacity, line height, font weight and layer order. | "opacity": 0.5, "lineHeight": 1.2 |
| Anything with another unit or a function is a quoted string. | "letterSpacing": "0.05em", "filter": "blur(4px)" |
| Commas between pairs, none after the last one. | { "opacity": 0.9, "borderRadius": 12 } |
How it combines with the controls
- Advanced is the same storage the controls use. Set a shadow with the Shadow control and it appears here as
boxShadow; type aboxShadowhere and the Shadow control shows it (or shows Custom if it's a shape the control can't edit, such as two shadows stacked). - A value typed here replaces the control's value for that property. Picking an option in a control replaces what you typed.
- Delete a line to go back to the default.
- The CSS goes on the item's own box - the whole item, including its padding and background.
- The published page uses exactly the same styles as the editor.
position, top, left, width, height or transform here. Freeform places, sizes and animates items with those, so setting them yourself fights with dragging, the phone layout, scroll effects and loops. Use Layout and Animate instead. (The one exception, "width": "max-content" for a sideways-scrolling strip, is set for you by the Loop control.)Phone-only and desktop-only styles
The second box, Per-device overrides, takes styles that apply to only one version of the page: "mobile" (the phone layout, screens up to 767px wide) or "desktop". They are added on top of the item's normal styles.
{
"mobile": { "fontSize": 28, "letterSpacing": "-0.01em" },
"desktop": { "textShadow": "0 2px 30px rgba(0, 0, 0, 0.4)" }
}Hide on phone (Properties → Layout) is stored here as "mobile": { "display": "none" } - so if you clear this box, a hidden item comes back on phones.
Sizes that grow with the page
Any size can be written as fluid(smallest, at width, largest, at width). The value slides between the two as the space the item sits in gets wider, and stops at each end.
{ "fontSize": "fluid(18, 375, 32, 1440)" }That text is 18px when its space is 375px wide, 32px at 1440px, and in between for sizes in between.
Built-in loop animations
The Loop control in the Animate tab covers these, but you can also write them as an animation value. Only these names work - you can't add your own keyframes.
| Name | What it does |
|---|---|
cv-float-y | Drifts up 14px and back. |
cv-spin | One full turn. |
cv-pulse | Grows slightly and brightens, then settles. |
cv-glow | A breathing coloured shadow. Its colour is "--cv-glow". |
cv-blink | On, off (use with steps(1)). |
cv-marquee, cv-marquee-rev | Slides sideways by half its width, for repeating strips. Needs "width": "max-content" and "whiteSpace": "nowrap" with the content typed twice (the Loop control sets both for you). |
{
"animation": "cv-glow 3s ease-in-out 0.5s infinite",
"--cv-glow": "rgba(236, 72, 153, 0.5)"
}The parts are: name, how long one loop takes, speed curve, an optional start delay, then infinite (and optionally reverse).
Recipes
| Look | CSS |
|---|---|
| Two shadows stacked (lifted card with a fine highlight) | "boxShadow": "0 20px 60px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.08)" |
| Glowing text | "textShadow": "0 0 20px #8b7bff, 0 0 40px #8b7bff" |
| Colour wheel / conic gradient | "backgroundImage": "conic-gradient(from 0deg, #f43f5e, #f59e0b, #10b981, #3b82f6, #f43f5e)" |
| Blend into what’s behind | "mixBlendMode": "screen" |
| Black-and-white image | "filter": "grayscale(1) contrast(1.1)" |
| Your own shape | "clipPath": "polygon(0 0, 100% 10%, 100% 100%, 0 90%)" |
| Several effects at once | "filter": "blur(2px) saturate(1.4)" |
| Text with a tiny cut-out look | "WebkitTextStroke": "1px #ffffff", "color": "transparent" |
What custom CSS can't do
- No hover, focus or other states - the styles always apply. (Buttons have hover effects built into their Style setting.)
- No selectors,
::before/::after,@mediaor@keyframes- only properties of the item itself. Use Per-device overrides instead of@media, and the built-in loops instead of your own keyframes. - Images in
url(...)must be a publichttps://address. - Styling inside text (one word bold or coloured) is done with the text's own formatting bar, not here.
Troubleshooting
| Problem | Fix |
|---|---|
| “That is not valid JSON, so nothing was changed.” | Check for single quotes (use double), a missing comma between pairs, a comma after the last pair, or a missing { or }. |
| Nothing changes | Check the property name is camelCase and spelt correctly - an unknown name is ignored by the browser. Check the value is valid CSS for that property. |
| A control shows “Custom” | The value came from here and the control can’t edit that shape. Edit it here, or pick an option in the control to replace it. |
| A frame’s fill doesn’t show | A frame paints its own fill on top of a CSS background. Use the Fill control (Properties → Appearance): it sets the fill and clears the frame’s own. |
| It looks different on phones | Check Per-device overrides for a "mobile" entry. |