Most motion use cases can be implemented by using css transitions. See Complex Motion to solve more advanced use cases.
#Easing, duration and delay
Several helpers offer access to the different easings, durations and delays.
Easing explains the easing curves and when to use them. Read about Duration and delays and which you should choose.
- Durations range from 100 ms - 1000 ms
- Delays range from 10 ms - 1000 ms
import { delay, duration, easing } from "@blocks/theme";
const Element = styled.div<{ $isVisible: boolean }>`
opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)};
transition: opacity ${duration[200]} ${easing.linear} ${delay[50]};
`;
To use durations and delays outside of CSS, use delayInMs
and durationInMs.
import { delayInMs, durationInMs } from "@blocks/theme";
const Component = () => {
const calculation = durationInMs[200] + delayInMs[50];
// ...
};
#Enter
Show codeHide code
export const KeepInDomDemo = () => {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const { ref, keepInDom } = useKeepInDom<HTMLDialogElement>(isDialogOpen);
const thumbnails: ReactNode[] = [];
for (let index = 0; index < 3; index++) {
thumbnails.push(
<ThumbnailContainer
key={index}
onClick={() => {
setIsDialogOpen(true);
}}
>
<Thumbnail />
</ThumbnailContainer>,
);
}
return (
<>
<Container>{thumbnails}</Container>
{keepInDom && (
<StyledDialog
$isVisible={isDialogOpen}
ref={ref}
onClick={() => setIsDialogOpen(false)}
>
<ImageContainer>
<Image />
</ImageContainer>
</StyledDialog>
)}
</>
);
};
Use @starting-style
to define the initial styles when adding the element to the DOM.
import { duration, easing } from "@blocks/theme";
const EnteringElement = styled.div`
transition: opacity ${duration[200]} ${easing.linear};
@starting-style {
opacity: 0;
}
`;
Use transition-behavior: allow-discrete
when transitioning
from display: none
or content-visibility: hidden
.
import { duration, easing } from "@blocks/theme";
const EnteringElement = styled.div<{ $isVisible: boolean }>`
transition:
/**
* Make sure the time is the same
* as the full animation you want to show
* Shorthand usage of transition-behavior
*/
display ${duration[200]} ${easing.linear} allow-discrete,
opacity ${duration[200]} ${easing.linear};
display: ${({ $isVisible }) => ($isVisible ? "block" : "none")};
@starting-style {
opacity: 0;
}
`;
#Exit
Use the useKeepInDom
hook if you want to animate an element
and its children before taking them out of the DOM.
import { useKeepInDom } from "@blocks/motion";
import { duration, easing } from "@blocks/theme";
export const ExitDemo = () => {
const [isVisible, setIsVisible] = useState(true);
// hook to keep element in DOM when a transition is occuring
const { ref, keepInDom } = useKeepInDom<HTMLDivElement>(isVisible);
return (
<>
<button onClick={() => setIsVisible(false)}>Hide Element</button>
{keepInDom && <ExitingElement $isVisible={isVisible} ref={ref} />}
</>
);
};
const ExitingElement = styled.div<{ $isVisible: boolean }>`
opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)};
transition: opacity ${duration[200]} ${easing.linear};
`;
Use transition-behavior: allow-discrete
when transitioning
to display: none
or content-visibility: hidden
. See the Enter example above.
#Staggering
Use a css variable to create a staggering effect for a list of items.
Show codeHide code
export const CountryDropdownDemo = () => {
const [selectedItem, setSelectedItem] = useState<ISelectOption | null>(null);
return (
<>
<Dropdown
ariaLabel="Country"
placeholder="Please choose"
items={countryOptions}
value={selectedItem}
onChange={(e) => setSelectedItem(e.target.value)}
/>
<div style={{ height: 250 }} />
</>
);
};
import { delay, duration, easing } from "@blocks/theme";
import { createVar } from "css-variable";
/** create the css variable */
export const staggeredIndex = createVar("staggered-index");
export const StaggeredItems = () => {
const items: ReactNode[] = [];
for (let index = 0; index < 6; index++) {
items.push(
/** set the css variable to a different value for each item */
<StaggeredItem key={index} style={staggeredIndex.toStyle(String(index))}>
Item {index}
</StaggeredItem>,
);
}
return <>{items}</>;
};
const StaggeredItem = styled.div`
/** use the css variable to calculate the different delay for each item */
transition: opacity ${duration[200]} ${easing.linear}
calc(${delay[50]} + ${staggeredIndex} * ${delay[20]});
@starting-style {
opacity: 0;
}
`;
#Reduced motion
The prefers-reduced-motion
media query can be used to replace motion effects.
Use the helper reducedMotion
to set prefers-reduced-motion
to reduce
or noMotionPreference
to set it to no-preference
respectively.
import {
delay,
duration,
easing,
noMotionPreference,
reducedMotion,
} from "@blocks/theme";
export const MovingWhileEnteringAndExiting = styled.div<{
$isVisible: boolean;
}>`
opacity: ${({ $isVisible }) => ($isVisible ? 1 : 0)};
transition:
translate ${duration[350]} ${duration.standard},
opacity ${duration[200]} ${duration.linear} ${delay[50]};
@starting-style {
opacity: 0;
translate: 0 -40px;
}
/** Make sure the movement while exiting is only applied when
* no motion preference is set. Otherwise it will immediately
* move to the new position when the transition is not set for translate. /*
${noMotionPreference} {
translate: ${({ $isVisible }) => ($isVisible ? "0 0" : "0 -40px")};
}
/** only transition the opacity without the movement */
${reducedMotion} {
transition: opacity ${duration[200]} ${easing.linear};
}
`;
import { duration, reducedMotion } from "@blocks/theme";
export const ChevronDown = styled(ChevronDownIcon)<{
$isRotated?: boolean;
}>`
transform: ${({ $isRotated }) =>
$isRotated ? "rotate(180deg)" : "rotate(0)"};
transition: transform ${duration[400]};
/** completely deactivate a transition */
${reducedMotion} {
transition: none;
}
`;