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;
  }
`;