Transitions and animations
Motion tells the user what just changed: a button that lifts, a panel that opens, a loader that spins. CSS can do all of it without a script. This lesson goes from the simplest case, a transition on hover, through transforms and 3D, to keyframe animations, animations driven by scrolling, and the care you owe to people who do not want things to move.
Transitions
Without a transition, a property changes in one frame: a hovered card is suddenly lower, a button is suddenly red. A transition tells the browser to move from the old value to the new one over some time. It runs whenever the value changes, whatever the cause: a hover, a focus, a class that a script toggles. The transition property is a shorthand for four things in this order: the property to animate, the duration, the timing function and the delay. Several transitions are separated by commas.
page:
title = "A hover transition"
global: selector = "body" font family = "system-ui, sans-serif" margin = 30 background = "#f1f5f9" end
box:
width = 240
padding = 20
background = white
border radius = 12
box shadow = "0 1px 3px rgba(0, 0, 0, 0.25)"
transition = "transform 200ms ease-out, box-shadow 200ms ease-out"
hover transform = "translateY(-6px)"
hover box shadow = "0 14px 24px rgba(0, 0, 0, 0.25)"
heading: size = 3 margin = "0 0 8" content = "Hover over me" end
paragraph: margin = 0 content = "The card lifts and its shadow grows over 200 milliseconds." end
end
endThe card moves and its shadow grows, each over 200 milliseconds with ease-out, which starts fast and ends gently. Put the transition on the normal state and not on the hover state: then the card also glides back when the pointer leaves. The longhands set one part each: transition property names the properties (all means every one, which is convenient and wasteful, so list the ones you mean), transition duration is a time such as 200ms or 0.3s, transition delay waits before it starts, and transition timing function is the speed curve.
The curve matters more than most people expect. linear is a constant speed and feels mechanical. ease is the default, ease-in starts slowly, ease-out ends slowly and suits things that arrive, and ease-in-out does both. cubic-bezier(x1, y1, x2, y2) draws your own curve, and when y goes above 1 or below 0 the movement overshoots and settles back. steps(n) moves in n jumps, like a clock hand, and is the base of sprite animations. Press Run to see five of them side by side.
import dom
func run():
dom.ToggleClass(dom.ById("lanes"), "go")
end
component Lane(timing):
box:
class = "track"
position = relative
height = 28
margin = "6 0"
background = "#e2e8f0"
border radius = 14
box: width = 28 height = 28 border radius = 14 background = "#0f766e" transition = "transform 1.5s " + timing end
paragraph: position = absolute top = 4 left = 40 margin = 0 font size = 13 content = timing end
end
end
page:
title = "Timing functions"
style: content = ".go .track > div { transform: translateX(260px) }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 20 end
box:
id = "lanes"
max width = 340
Lane: timing = "linear" end
Lane: timing = "ease-in" end
Lane: timing = "ease-out" end
Lane: timing = "steps(5)" end
Lane: timing = "cubic-bezier(0.3, 1.6, 0.6, 1)" end
end
button: margin top = 10 click = run content = "Run" end
endOnly properties with a value in between can transition: colors, lengths, opacity, transforms and so on. A switch from display: none to block has nothing in between, so by default it happens at once. transition behavior = "allow-discrete" lets such properties join a transition, so the change happens at the right end of it, and together with the raw @starting-style rule it lets an element fade in when it first appears.
A transition needs two different values. If an element is created already in its final state, nothing moves. And when you give only the hover state a transition, the way back is instant, which is a common reason for a half finished feeling.
Transforms
transform moves, turns, scales and skews an element after the layout is done. It takes one or more functions: translate(x, y) moves, rotate(angle) turns by degrees (45deg) or turns (0.25turn), scale(n) multiplies the size, skew(x, y) slants it, and matrix(a, b, c, d, e, f) is all of them as numbers. The functions apply from the right to the left. In translate(100px) rotate(45deg) the element is turned first and then moved 100 pixels to the right, and in rotate(45deg) translate(100px) it is moved along its own, already turned axis, so it ends up diagonally. Swap the functions and you get another picture.
component Tile(label, move, from):
box:
width = 90
height = 60
display = grid
place items = center
background = "#bfdbfe"
border = "2px solid #1d4ed8"
transform = move
transform origin = from
content = label
end
end
page:
title = "Transform functions"
global: selector = "body" font family = "system-ui, sans-serif" margin = 30 end
box:
display = flex
flex wrap = wrap
gap = 36
Tile: label = "translate" move = "translate(20px, 10px)" from = "center" end
Tile: label = "rotate" move = "rotate(-12deg)" from = "center" end
Tile: label = "scale" move = "scale(1.25)" from = "center" end
Tile: label = "skew" move = "skew(-15deg, 0)" from = "center" end
Tile: label = "origin" move = "rotate(25deg)" from = "top left" end
box:
width = 90
height = 60
display = grid
place items = center
background = "#fde68a"
border = "2px solid #b45309"
rotate = "8deg"
scale = 1.1
translate = "0 12px"
content = "separate"
end
end
endA transformed element keeps its place in the layout: its neighbours do not move, and the picture of the element is drawn somewhere else. That is exactly why transforms are the best tool for animation. The turn happens around the middle of the element unless transform origin says otherwise: the tile called origin turns around its top left corner. transform box chooses the box that the origin is measured in, which matters for SVG pictures, where fill-box means the element's own shape.
The last tile uses the three individual properties, translate, rotate and scale. They do what the functions do, and apply in that order before transform. Their use is that one animation can change rotate while a hover changes scale, and neither overwrites the other, which happens when both write the same transform.
Perspective and 3D
Add a third dimension and you can turn a card over. perspective on the parent says how far the viewer is from the page, in pixels: a small number like 300 gives a strong, dramatic depth and a large one a mild depth. perspective origin is where the viewer's eye is, by default the middle. A child is then turned with rotateX or rotateY around the horizontal or vertical axis. For the faces of a card to be two real sides, the turning element needs transform style = "preserve-3d", which keeps its children in the same 3D space instead of flattening them into its own plane, and each face needs backface visibility = hidden, which hides a face while its back is toward you.
import dom
mut flipped := false
func flip():
flipped = not flipped
if flipped:
dom.SetStyle(dom.ById("card"), "transform", "rotateY(180deg)")
else:
dom.SetStyle(dom.ById("card"), "transform", "rotateY(0deg)")
end
end
page:
title = "A flipping card"
global: selector = "body" font family = "system-ui, sans-serif" margin = 30 end
box:
width = 200
height = 120
perspective = 800
button:
id = "card"
click = flip
position = relative
width = "100%"
height = "100%"
padding = 0
border = none
background = none
font size = 18
transform style = "preserve-3d"
transition = "transform 600ms"
span: display = block position = absolute top = 0 left = 0 width = "100%" height = "100%" line height = "120px" border radius = 12 background = "#0f766e" color = white backface visibility = hidden content = "Front" end
span: display = block position = absolute top = 0 left = 0 width = "100%" height = "100%" line height = "120px" border radius = 12 background = "#f59e0b" transform = "rotateY(180deg)" backface visibility = hidden content = "Back" end
end
end
endClick the card. The front is a normal face, and the back is turned half way round in advance with rotateY(180deg), so when the whole card turns it comes to the front. A button makes the card reachable by keyboard and screen readers. The handler changes the transform by script, because the state (turned or not) lives in a variable, and the transition does the movement.
Some properties on the turning element flatten preserve-3d and spoil the effect: overflow other than visible, opacity below 1 and filter. If the back face is missing, look for one of them.
Keyframe animations
A transition goes from A to B when something changes. An animation runs by itself, as often as you say, and can pass through any number of steps. It has two parts: the keyframes, which say what the element looks like at points of the animation, and the animation properties on the element, which say which keyframes to play and how. The keyframes are written as an @keyframes rule with a name. Salam has no element for it, because a keyframe list is not part of any element: it is one named list for the whole page that any element may use. So it is raw CSS inside a style element.
page:
title = "A spinner"
style: content = "@keyframes spin { to { transform: rotate(1turn) } }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 30 end
box:
width = 48
height = 48
border = "6px solid #e2e8f0"
border top color = "#0f766e"
border radius = "50%"
animation = "spin 1s linear infinite"
end
paragraph: content = "Loading" end
endThe keyframes say only where to end: to { transform: rotate(1turn) }. The start is the element's normal state. The animation shorthand on the box names the keyframes and then gives the duration, the timing function and the iteration count: one second, constant speed, forever. The same thing, written with the longhands, is easier to read and to change. Each one gets its own line below, and the dots get a different animation delay so that they bounce one after another.
component Dot(wait):
box:
width = 16
height = 16
border radius = "50%"
background = "#1d4ed8"
animation name = bounce
animation duration = "0.5s"
animation timing function = "ease-in-out"
animation iteration count = infinite
animation direction = alternate
animation delay = wait
end
end
page:
title = "Bouncing dots"
style: content = "@keyframes bounce { from { transform: translateY(0) } to { transform: translateY(-28px) } }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 30 end
box:
display = flex
gap = 10
align items = "flex-end"
height = 50
Dot: wait = "0s" end
Dot: wait = "0.15s" end
Dot: wait = "0.3s" end
end
endThe longhands are: animation name, the keyframes to play; animation duration, the time of one round; animation timing function, the curve of each step between keyframes; animation delay, the wait before the start (a negative delay starts the animation already part of the way through); animation iteration count, a number or infinite; and animation direction. Direction alternate plays forward then backward, which is why the dots go up and down and not up and jump down. reverse plays backward and alternate-reverse starts backward.
Before the start and after the end, an element looks as if there were no animation. That is surprising for things that appear. With a delay, the element is first fully visible, then it jumps to the first keyframe, then it animates, and at the end it jumps back. animation fill mode changes this: backwards applies the first keyframe during the delay, forwards keeps the last keyframe after the end, and both does both. The list below uses both so that each row waits invisibly and then stays in place. Replay restarts it.
import dom
func replay():
dom.AddClass(dom.ById("list"), "off")
dom.SetTimeout((): dom.RemoveClass(dom.ById("list"), "off") end, 60)
end
component Row(text, wait):
box:
padding = 12
margin = "0 0 8"
background = "#e0f2fe"
border radius = 8
animation name = rise
animation duration = "0.5s"
animation fill mode = both
animation delay = wait
content = text
end
end
page:
title = "Entering one by one"
style: content = "@keyframes rise { from { opacity: 0; transform: translateY(16px) } to { opacity: 1; transform: none } } .off > div { animation: none }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
box:
id = "list"
max width = 320
Row: text = "First" wait = "0s" end
Row: text = "Second" wait = "0.2s" end
Row: text = "Third" wait = "0.4s" end
end
button: click = replay content = "Replay" end
endThe Replay button adds a class that turns the animation off for one moment and then removes it, because an animation restarts when its name is removed and set again. In the page, .off > div is a raw rule that sets animation: none while the class is on.
animation play state is running or paused, and pausing keeps the animation where it is. Anything that moves by itself for more than a few seconds should have a way to stop it, for people who cannot read while something moves. The next page does it with one line of script, dom.SetStyle(el, "animation-play-state", "paused").
import dom
mut running := true
func toggle():
running = not running
if running:
dom.SetStyle(dom.ById("ring"), "animation-play-state", "running")
dom.SetText(dom.ById("btn"), "Pause")
else:
dom.SetStyle(dom.ById("ring"), "animation-play-state", "paused")
dom.SetText(dom.ById("btn"), "Play")
end
end
page:
title = "Pause an animation"
style: content = "@keyframes spin { to { transform: rotate(1turn) } }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 30 end
box:
id = "ring"
width = 48
height = 48
border = "6px solid #e2e8f0"
border top color = "#be123c"
border radius = "50%"
animation = "spin 1.2s linear infinite"
end
button: id = "btn" margin top = 16 click = toggle content = "Pause" end
endOne more property, animation composition, matters when an element already has a value and the animation adds to it. replace, the default, ignores the underlying value, add adds the animated value to it, and accumulate does the same for lists such as several transform functions. It lets two animations that both write transform work together.
Smooth and cheap
An animation is redrawn about 60 times a second, so each frame has little more than 16 milliseconds. Changing width, height, top or left makes the browser lay out the page again on every frame, and on a phone that stutters. Changing transform and opacity usually does not need layout or even painting: the browser keeps the element as a picture on the graphics card and only moves it. Whenever you can, move things with translate and hide them with opacity.
will change is a hint that a property is about to change, such as will change = transform, so the browser can prepare that picture early. Use it on a few elements just before they animate, and remove it afterwards: every element prepared that way costs memory, and a page that says it about everything gets slower.
Motion along a path
A transform can only go in straight lines, and a curve needs a different tool. offset path gives the element a path to follow, most simply path('M 0 40 C 60 -40, 120 120, 200 40') with the commands of an SVG path. offset distance says how far along the path it is, from 0% to 100%, and that is the value to animate. offset rotate decides the direction it faces: auto follows the path like a car, and an angle keeps it fixed. offset anchor picks the point of the element that sits on the path, and offset position the starting point for shapes that need one. offset sets several of them at once.
page:
title = "Motion along a path"
style: content = "@keyframes drive { to { offset-distance: 100% } }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 30 end
box:
height = 130
width = 260
padding = "40 0 0 20"
box sizing = "border-box"
border = "1px dashed #94a3b8"
box:
width = 28
height = 20
background = "#f59e0b"
clip path = "polygon(0 0, 100% 50%, 0 100%)"
offset path = "path('M 0 40 C 60 -40, 120 120, 200 40')"
offset rotate = auto
animation = "drive 3s ease-in-out infinite alternate"
end
end
endThe arrow is a box cut with clip path, and its distance goes from 0 to 100 percent in the keyframes, back and forth with alternate. The turn of the arrow comes from offset rotate = auto.
Animations driven by scrolling
An animation normally follows the clock. With animation timeline it can follow something else, such as how far the page is scrolled, with no script at all. scroll() is the progress of the nearest scrolling ancestor, from the top to the end. The animation then plays from start to end as the user scrolls, and the duration plays no role. The bar below grows from nothing to the full width as the page is read.
page:
title = "A reading progress bar"
style: content = "@keyframes fill { from { transform: scaleX(0) } to { transform: scaleX(1) } }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 0 line height = 1.7 end
box:
position = fixed
top = 0
left = 0
width = "100%"
height = 6
background = "#0f766e"
transform origin = "0 50%"
animation name = fill
animation timing function = linear
animation timeline = "scroll()"
end
box:
padding = 24
heading: size = 2 content = "Scroll down" end
paragraph: repeat = 14 content = "The bar at the top grows with the scroll position. No script reads the scroll, the browser connects the animation to it." end
end
endview() is the progress of the element itself through the scroller: it starts when the element begins to enter the view and ends when it has left. animation range picks the part that counts, with names such as entry (while it enters), exit, contain and cover (the whole way through), each with percentages. animation range start and end set the two ends one by one. Scroll the page below: each box fades in while it is entering and is finished when it has fully entered.
page:
title = "Reveal on scroll"
style: content = "@keyframes show { from { opacity: 0; transform: translateY(30px) } to { opacity: 1; transform: none } }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
heading: size = 2 content = "Scroll down" end
box:
height = 160
margin = "0 0 40"
padding = 16
background = "#e0f2fe"
border radius = 10
animation name = show
animation timing function = linear
animation fill mode = both
animation timeline = "view()"
animation range = "entry 0% entry 100%"
repeat = 6
content = "I fade in while I enter the screen"
end
endWhen two elements are not nested, one can use the timeline of the other by name. scroll timeline name and axis (or the shorthand scroll timeline) give a scroller's progress a name that starts with two hyphens, view timeline name, axis and inset (or view timeline) do the same for the progress of an element, and animation timeline = "--name" uses it. A name is visible only to the descendants of the element that defines it, and timeline scope on a common ancestor lifts that limit. view timeline inset shrinks the visible area by some pixels at the top and bottom.
Scroll driven animations work in Chromium based browsers such as Chrome and Edge, and not in every other browser yet. Without support the animated element simply stays in its normal state, so write them as decoration that the page does not depend on, and keep the content readable without them. Write animation timeline after the animation shorthand, because the shorthand resets it.
View transitions
A view transition animates a change of the page, even a big one, with almost no work from you. A script calls document.startViewTransition(f) with the function that changes the page. The browser takes a picture of the old page, runs f, takes a picture of the new page, and cross fades between them. An element with a view transition name is treated separately: its old and new picture are moved and scaled from the old place and size to the new, so it seems to travel. The name must be unique on the page at that time. view transition class gives several elements one class name, so one rule can style all their transitions.
page:
title = "A view transition"
style: content = ".wide { width: 240px !important; height: 100px !important; background: #f59e0b !important } ::view-transition-group(tile) { animation-duration: 500ms }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 30 end
box:
id = "tile"
width = 80
height = 80
background = "#0f766e"
border radius = 12
view transition name = tile
end
button: margin top = 16 click = "var f = function () { document.getElementById('tile').classList.toggle('wide') }; if (document.startViewTransition) { document.startViewTransition(f) } else { f() }" content = "Morph" end
endThe page changes class on the tile, and the browser animates it from small and green to big and orange. The raw rule on ::view-transition-group(tile) makes the movement last 500 milliseconds. The script asks first whether the browser has startViewTransition: in a browser without it the change simply happens at once. View transitions exist in recent Chromium and Safari browsers.
Animating to auto
A panel that opens to the height of its text is the classic case that CSS could not animate: the height auto is not a number, so there was nothing between 0 and auto. interpolate size = "allow-keywords" allows it, for auto, min-content, max-content and fit-content. The property is inherited, so one rule on :root switches it on for the page, as the example does with a global rule. Where the browser does not know it, the panel still opens, but at once.
import dom
mut open := false
func toggle():
open = not open
if open:
dom.SetStyle(dom.ById("panel"), "height", "auto")
else:
dom.SetStyle(dom.ById("panel"), "height", "0")
end
end
page:
title = "Animating to auto"
global: selector = ":root" interpolate size = "allow-keywords" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
button: click = toggle content = "Show details" end
box:
id = "panel"
height = 0
overflow = hidden
transition = "height 400ms ease"
paragraph: margin = "12 0" content = "The panel is as tall as its text. Its height goes from 0 to auto, which CSS could not animate before." end
end
endReduced motion
For some people large movement, parallax and spinning cause dizziness and nausea. Operating systems have a setting for it, and browsers pass it to your page as the prefers-reduced-motion media query. Respecting it is part of building a page: it costs a few lines.
page:
title = "Respecting reduced motion"
style: content = "@keyframes pulse { 50% { transform: scale(1.25); opacity: 0.6 } } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important } }" end
global: selector = "body" font family = "system-ui, sans-serif" margin = 30 end
box:
width = 40
height = 40
border radius = "50%"
background = "#be123c"
animation = "pulse 1.2s ease-in-out infinite"
end
paragraph: content = "This dot pulses, unless your system asks for reduced motion." end
endThe raw rule at the end of the style says that, when the user asked for less motion, every animation and transition is off. It uses !important so that it also wins over the rules the compiler writes later. It is a blunt tool and a good first step. A finer one keeps the meaning and removes the movement: a panel that slid in can fade in, and a long travel can become a short one. The same query decides whether scroll behavior = smooth from the previous lesson should be on at all. Never let anything flash more than three times a second, whatever the user's settings are.
Reference: transition, transform and animation properties
The properties are in groups. Every example is checked by the compiler, so you can paste it into a page: block, and the animation examples that use names such as spin need the matching @keyframes rule in a style element, as in the lessons above. The accepted values are in brackets after each description.
Transitions
| Property | What it does and what it accepts | Example |
|---|---|---|
transitionگذار transition | Shorthand for a whole transition: the property, the duration, the timing function and the delay, in this order. With two times, the first is the duration and the second the delay. Several transitions are separated by commas. [comma list] | button:
padding = "8 18"
background = "#1d4ed8"
color = white
transition = "background 200ms ease-out, transform 100ms"
hover background = "#1e3a8a"
active transform = "scale(0.96)"
content = "Press me"
end |
transition propertyویژگی گذار transition-property | Names the properties that transition: one or more names separated by commas, all for every property that can change, or none. List the properties you mean, because all also animates things you did not think of. [name / none, all; comma list] | box:
width = 100
height = 40
background = "#bfdbfe"
transition property = "width, background"
transition duration = "300ms"
hover width = 200
hover background = "#93c5fd"
content = "Only width and color glide"
end |
transition durationمدت گذار transition-duration | How long a transition takes, as a time such as 200ms or 0.3s. Several times, separated by commas, go with several properties in order. The default 0s means no transition. [time; comma list] | box:
padding = 12
background = "#fde68a"
transition = "background"
transition duration = "600ms"
hover background = "#f59e0b"
content = "Slowly turns orange"
end |
transition timing functionتابع زمانبندی گذار transition-timing-function | The speed curve of a transition. Keywords: ease (the default), linear, ease-in, ease-out, ease-in-out, step-start and step-end. Functions: cubic-bezier(x1, y1, x2, y2) for your own curve, steps(n) for n jumps and linear(...) for a list of points. [timing function / ease, linear, ease-in, ease-out, ease-in-out, step-start, ...; comma list] | box:
width = 40
height = 40
background = "#0f766e"
transition = "transform 1s"
transition timing function = "cubic-bezier(0.3, 1.6, 0.6, 1)"
hover transform = "translateX(120px)"
end |
transition delayتاخیر گذار transition-delay | How long to wait before a transition starts, as a time. A negative time makes it start already part of the way through. Several times, separated by commas, go with several properties. [time; comma list] | box:
padding = 12
background = "#e2e8f0"
transition = "background 300ms"
transition delay = "500ms"
hover background = "#fca5a5"
content = "Waits half a second, then changes"
end |
transition behaviorرفتار گذار transition-behavior | Chooses whether properties that cannot be blended, such as display, take part in a transition. normal leaves them out, so they change at once, and allow-discrete lets them change at the right end of the transition. Several values, separated by commas, go with several properties. [normal, allow-discrete; comma list] | box:
padding = 12
background = "#e0f2fe"
transition = "opacity 400ms, display 400ms"
transition behavior = "allow-discrete"
hover opacity = 0.3
content = "Discrete properties join the transition"
end |
Transforms and 3D
| Property | What it does and what it accepts | Example |
|---|---|---|
transformدگرگونی transform | Moves, turns, scales or skews the element after the layout is done. A list of functions, such as translate(10px, 20px), rotate(45deg), scale(1.5), skew(10deg), matrix(...) and the 3D ones rotateX(), rotateY() and translateZ(). They apply from the right to the left. Neighbours keep their places. [up to 99 values] | box:
width = 120
padding = 16
background = "#bbf7d0"
transform = "rotate(-6deg) scale(1.1)"
content = "Turned and enlarged"
end |
translateجابهجایی translate | Moves the element, like transform: translate(), as a property of its own. One, two or three lengths or percentages for x, y and z. Percentages refer to the size of the element itself. none removes it. [length / percentage / none; up to 3 values] | box:
width = 100
padding = 12
background = "#fecaca"
translate = "30px 10px"
content = "Moved aside"
end |
rotateچرخش rotate | Turns the element, like transform: rotate(), as a property of its own. An angle such as 45deg or 0.25turn, or an axis and an angle such as y 60deg for a 3D turn. none removes it. [angle / none] | box:
width = 100
padding = 12
background = "#ddd6fe"
rotate = "15deg"
content = "Slightly turned"
end |
scaleمقیاس scale | Scales the element, like transform: scale(), as a property of its own. One number or percentage for both directions, two for x and y, three for x, y and z. none removes it. [number / percentage / none; up to 3 values] | box:
width = 100
padding = 12
background = "#fde68a"
scale = 1.2
content = "Larger by a fifth"
end |
transform originمبدا دگرگونی transform-origin | The point that turning and scaling is measured from. One to three values: keywords such as top left and center, lengths or percentages. The default is 50% 50%, the middle of the element. [length / percentage; up to 3 values] | box:
width = 100
padding = 12
background = "#bae6fd"
transform = "rotate(20deg)"
transform origin = "top left"
content = "Turns around its corner"
end |
transform boxجعبه دگرگونی transform-box | Chooses which box the transform origin and percentages are measured in. border-box and content-box are for normal elements, and fill-box, stroke-box and view-box for SVG shapes, where fill-box means the shape itself. [content-box, border-box, fill-box, stroke-box, view-box] | box:
width = 100
padding = 12
background = "#fbcfe8"
transform box = "border-box"
transform origin = "100% 0"
transform = "rotate(-10deg)"
content = "Origin at the top right corner"
end |
transform styleسبک دگرگونی transform-style | Says whether the children of a transformed element live in the same 3D space (preserve-3d) or are flattened into the element's plane (flat, the default). A card with two faces needs preserve-3d. overflow, opacity below 1 and filter flatten it again. [flat, preserve-3d] | box:
perspective = 600
box:
width = 120
height = 60
background = "#99f6e4"
transform style = "preserve-3d"
transform = "rotateY(35deg)"
content = "A 3D scene"
end
end |
perspectiveپرسپکتیو perspective | The distance of the viewer from the page, set on the parent of the 3D elements. A length: small values such as 300px give strong depth and large ones a mild depth. none is flat. [length / none] | box:
perspective = 500
box:
width = 120
padding = 12
background = "#c7d2fe"
transform = "rotateY(40deg)"
content = "Seen in perspective"
end
end |
perspective originمبدا پرسپکتیو perspective-origin | The position of the viewer's eye in front of the element that has perspective, where the lines of depth meet. Keywords, lengths or percentages; the default is the middle, 50% 50%. [length / percentage; up to 4 values] | box:
perspective = 500
perspective origin = "left top"
box:
width = 120
padding = 12
background = "#fed7aa"
transform = "rotateX(40deg)"
content = "Seen from the top left"
end
end |
backface visibilityدیدپذیری پشت backface-visibility | Says whether the back of an element is drawn when it is turned away from the viewer. visible is the default and shows a mirror image, hidden shows nothing. It makes the two faces of a flipping card possible. [visible, hidden] | box:
width = 120
padding = 12
background = "#a7f3d0"
backface visibility = hidden
transform = "rotateY(180deg)"
content = "You cannot see my back"
end |
Animations
| Property | What it does and what it accepts | Example |
|---|---|---|
animationپویانمایی animation | Shorthand that starts a keyframe animation: the name, the duration, the timing function, the delay, the iteration count, the direction, the fill mode and the play state, where only the name and the duration are needed. The @keyframes rule with that name goes in a style element. Several animations are separated by commas. [comma list] | box:
width = 40
height = 40
border radius = "50%"
background = "#be123c"
animation = "pulse 1.2s ease-in-out infinite"
end |
animation nameنام پویانمایی animation-name | The name of the @keyframes rule to play, or none. Several names, separated by commas, play several animations on the same element. [name / none; comma list] | box:
width = 40
height = 40
background = "#0f766e"
animation name = slide
animation duration = "2s"
end |
animation durationمدت پویانمایی animation-duration | The time of one round of the animation, such as 2s or 300ms. The default 0s plays nothing. For a scroll driven animation the duration is not used. [time / auto; comma list] | box:
width = 40
height = 40
background = "#7c3aed"
animation name = slide
animation duration = "800ms"
end |
animation timing functionتابع زمانبندی پویانمایی animation-timing-function | The speed curve between each pair of keyframes. The same values as for transitions: ease, linear, ease-in, ease-out, ease-in-out, cubic-bezier(), steps(). A curve can also be set inside a keyframe. [timing function / ease, linear, ease-in, ease-out, ease-in-out, step-start, ...; comma list] | box:
width = 40
height = 40
background = "#ea580c"
animation name = slide
animation duration = "2s"
animation timing function = "steps(6)"
end |
animation delayتاخیر پویانمایی animation-delay | The wait before the animation starts, as a time. A negative time starts it already part of the way through, which is the way to offset several copies of the same animation. [time; comma list] | box:
width = 40
height = 40
background = "#0891b2"
animation name = slide
animation duration = "2s"
animation delay = "-1s"
end |
animation iteration countتعداد تکرار پویانمایی animation-iteration-count | How many times the animation plays: a number, a fraction such as 1.5, or infinite. The default is 1. [number / infinite; comma list] | box:
width = 40
height = 40
background = "#65a30d"
animation name = slide
animation duration = "1s"
animation iteration count = 3
end |
animation directionجهت پویانمایی animation-direction | The direction of each round. normal goes from the first to the last keyframe, reverse goes backward, alternate goes forward then backward, and alternate-reverse starts backward. [normal, reverse, alternate, alternate-reverse; comma list] | box:
width = 40
height = 40
background = "#db2777"
animation name = slide
animation duration = "1s"
animation iteration count = infinite
animation direction = alternate
end |
animation fill modeحالت پرکردن پویانمایی animation-fill-mode | What the element looks like outside the animation. none shows its normal style, forwards keeps the last keyframe after the end, backwards applies the first keyframe during the delay, and both does both. [none, forwards, backwards, both; comma list] | box:
padding = 12
background = "#e0f2fe"
animation name = fadein
animation duration = "600ms"
animation delay = "400ms"
animation fill mode = both
content = "Invisible until my turn, then stays"
end |
animation play stateوضعیت پخش پویانمایی animation-play-state | Runs or pauses the animation. running is the default and paused freezes it where it is. A script can switch it with dom.SetStyle(el, "animation-play-state", "paused"). [running, paused; comma list] | box:
width = 40
height = 40
background = "#475569"
animation = "spin 1s linear infinite"
animation play state = paused
end |
animation compositionترکیببندی پویانمایی animation-composition | Says how an animated value combines with the value the element already has. replace ignores it, add adds the two, and accumulate merges lists such as transform functions. Several values, separated by commas, go with several animations. [replace, add, accumulate; comma list] | box:
width = 40
height = 40
background = "#0369a1"
transform = "rotate(30deg)"
animation name = grow
animation duration = "1s"
animation composition = add
end |
Motion paths
| Property | What it does and what it accepts | Example |
|---|---|---|
offsetحرکت مسیر offset | Shorthand for motion along a path: the path, the distance along it, the rotation and, after a slash, the anchor. The starting position can come first. [path, distance, rotation, then / and an anchor position] | box:
width = 24
height = 24
background = "#f59e0b"
offset = "path('M 0 40 C 60 -40, 120 120, 200 40') 40% auto"
end |
offset pathمسیر حرکت offset-path | The path an element follows. path('...') takes the commands of an SVG path, ray(45deg) is a straight line from the starting position, and none removes it. Together with offset distance it moves the element. [up to 2 values] | box:
width = 24
height = 24
background = "#16a34a"
offset path = "path('M 0 0 L 200 60')"
offset distance = "50%"
end |
offset distanceفاصله مسیر offset-distance | How far the element is along its offset path: a length or a percentage of the path, 0% at the start and 100% at the end. This is the value that a motion path animation changes. [length / percentage] | box:
width = 24
height = 24
background = "#dc2626"
offset path = "path('M 0 0 L 200 0')"
offset distance = "75%"
end |
offset rotateچرخش مسیر offset-rotate | How the element turns while it moves. auto faces the direction of the path, reverse the opposite one, an angle such as 90deg keeps a fixed turn, and auto 90deg adds an angle to the path direction. [angle; up to 2 values] | box:
width = 28
height = 20
background = "#2563eb"
offset path = "path('M 0 40 C 60 -40, 120 120, 200 40')"
offset distance = "30%"
offset rotate = "auto"
end |
offset anchorلنگر حرکت offset-anchor | The point of the element that sits on the path. Keywords, lengths or percentages; the default auto uses the transform origin. [length / percentage; up to 4 values] | box:
width = 40
height = 20
background = "#9333ea"
offset path = "path('M 0 0 L 200 0')"
offset distance = "50%"
offset anchor = "left top"
end |
offset positionموقعیت حرکت offset-position | The starting point of a path that has none of its own, such as ray(). Keywords, lengths or percentages in the containing block; normal means no position and auto the element's own place. [length / percentage; up to 4 values] | box:
width = 20
height = 20
background = "#0d9488"
offset position = "40px 20px"
offset path = "ray(90deg)"
offset distance = "60px"
end |
Scroll driven animations
| Property | What it does and what it accepts | Example |
|---|---|---|
animation timelineخط زمان پویانمایی animation-timeline | The timeline that drives the animation instead of the clock. scroll() follows the scroll position of the nearest scroller, view() follows how far the element has moved through the visible area, a name such as --card uses a named timeline, auto is the clock and none removes it. Chromium based browsers only. Write it after the animation shorthand. [comma list of dashed idents, scroll(), view() / none, auto] | box:
height = 6
background = "#0f766e"
transform origin = "0 50%"
animation = "fill 1s linear"
animation timeline = "scroll()"
end |
animation rangeبازه پویانمایی animation-range | The part of a scroll driven timeline during which the animation plays, as a start and an end. Each is a name (entry, exit, contain, cover) with an optional percentage. Chromium based browsers only. [comma list of range names with optional length/percentage] | box:
padding = 16
background = "#e0f2fe"
animation = "show 1s linear both"
animation timeline = "view()"
animation range = "entry 0% cover 40%"
content = "Fades in while entering"
end |
animation range startآغاز بازه پویانمایی animation-range-start | Where in the timeline the animation starts: a name such as entry, exit, contain or cover, with an optional percentage or length, for example entry 20%. Chromium based browsers only. [comma list of a range name or length/percentage] | box:
padding = 16
background = "#fef3c7"
animation = "show 1s linear both"
animation timeline = "view()"
animation range start = "entry 20%"
content = "Starts at a fifth of the entry"
end |
animation range endانتهای بازه پویانمایی animation-range-end | Where in the timeline the animation ends: a name with an optional percentage or length, for example cover 50%. Chromium based browsers only. [comma list of a range name or length/percentage] | box:
padding = 16
background = "#dcfce7"
animation = "show 1s linear both"
animation timeline = "view()"
animation range end = "cover 50%"
content = "Done at the middle of the way"
end |
scroll timelineخط زمان پیمایش scroll-timeline | Shorthand that names the scroll progress of this element for other elements to use: the name, which begins with two hyphens, and then the axis. Chromium based browsers only. [dashed ident, optional axis, shorthand] | box:
height = 100
overflow y = auto
scroll timeline = "--page block"
paragraph: content = "Scroll inside me" end
paragraph: content = "and an animation with the timeline --page follows" end
paragraph: content = "line three" end
paragraph: content = "line four" end
end |
scroll timeline nameنام خط زمان پیمایش scroll-timeline-name | A name for the scroll progress of this element, beginning with two hyphens, such as --page. Another element uses it with animation timeline = "--page". none is the default. [comma list of dashed idents / none] | box:
height = 100
overflow y = auto
scroll timeline name = "--list"
paragraph: content = "A named scroller" end
paragraph: content = "second line" end
paragraph: content = "third line" end
paragraph: content = "fourth line" end
end |
scroll timeline axisمحور خط زمان پیمایش scroll-timeline-axis | Which direction of scrolling the named timeline measures: block (the direction in which lines stack), inline, or the physical x and y. [comma list of block / inline / x / y] | box:
width = 200
overflow x = auto
white space = nowrap
scroll timeline name = "--strip"
scroll timeline axis = "inline"
content = "A long line that scrolls sideways and drives a timeline called strip"
end |
view timelineخط زمان دید view-timeline | Shorthand that names the progress of this element through the scrolling area, for other elements to use: the name, which begins with two hyphens, the axis and the inset. Chromium based browsers only. [dashed ident, optional axis and inset, shorthand] | box:
padding = 16
background = "#fce7f3"
view timeline = "--card block"
content = "My progress has a name"
end |
view timeline nameنام خط زمان دید view-timeline-name | A name for how far this element has moved through the visible area of its scroller, beginning with two hyphens, such as --card. Another element can follow it with animation timeline. none is the default. [comma list of dashed idents / none] | box:
padding = 16
background = "#ede9fe"
view timeline name = "--card"
content = "Others can follow how visible I am"
end |
view timeline axisمحور خط زمان دید view-timeline-axis | Which direction the element's progress is measured in: block, inline, x or y. The default is block. [comma list of block / inline / x / y] | box:
padding = 16
background = "#cffafe"
view timeline name = "--item"
view timeline axis = "inline"
content = "Measured sideways"
end |
view timeline insetفرورفتگی خط زمان دید view-timeline-inset | Shrinks the visible area of the scroller by some distance at its start and end, so the timeline begins later and ends sooner. One or two lengths or percentages, or auto. [up to 2 lengths/percentages or auto] | box:
padding = 16
background = "#fef9c3"
view timeline name = "--tile"
view timeline inset = "40px"
content = "My timeline ignores the outer 40 pixels"
end |
timeline scopeدامنه خط زمان timeline-scope | Lifts the scope of named timelines to this element, so that elements outside the one that defines the timeline can follow it too. A list of names, all or none. Chromium based browsers only. [comma list of dashed idents / none, all] | box:
timeline scope = "--card"
paragraph: content = "Every element inside me can use the timeline --card" end
end |
Performance, view transitions and sizes
| Property | What it does and what it accepts | Example |
|---|---|---|
view transition nameنام گذار نما view-transition-name | Gives the element a name for view transitions, so that in a transition it is moved and scaled from its old place to its new one instead of cross fading with the rest of the page. The name must be unique on the page. none is the default. [name / none] | box:
width = 80
height = 80
background = "#0f766e"
view transition name = hero
end |
will changeتغییر پیشرو will-change | A hint that some properties are about to change, so the browser can prepare. A list of property names, such as transform or transform, opacity, or auto. Use it on few elements, just before they animate, because each hint costs memory. [name / auto; comma list] | box:
width = 80
height = 80
background = "#14b8a6"
will change = "transform"
transition = "transform 300ms"
hover transform = "translateY(-10px)"
end |
view transition classکلاس گذار دید view-transition-class | One or more class names for the element's view transition, so that a single rule such as ::view-transition-group(*.card) can style many elements with different names. none is the default. [space list of idents / none] | box:
width = 80
height = 80
background = "#f59e0b"
view transition name = tile-one
view transition class = card
end |
interpolate sizeمیانیابی اندازه interpolate-size | Lets lengths animate between a number and an intrinsic size keyword such as auto, min-content or max-content. allow-keywords turns it on and numeric-only is the default. It is inherited, so it is usually set once on :root. [numeric-only / allow-keywords] | box:
interpolate size = "allow-keywords"
overflow = hidden
height = 40
transition = "height 300ms"
hover height = auto
content = "My height opens up to the size of my text when you hover over me"
end |
Try it yourself
- Make a menu button whose icon turns by 90 degrees when it is pressed, using
transitionand a script that changesrotate. - Change the dots example to five dots, a duration of 0.8 seconds and
cubic-bezier(0.34, 1.56, 0.64, 1). What changes when you removealternate? - Write a loader that spins, and add a button that pauses it. Then add the reduced motion rule and test it by switching the setting in your system.
- Build a long page and give every section the reveal animation with
view(). Check that the page is still fully readable in a browser where it does not run.