Positioning elements

Position elements precisely using page dimensions and coordinates.

Running this example

To run this example locally:

  1. If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.

  2. In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to http://localhost:8080.

  3. Clone the starter kit:

    git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git
    cd canva-apps-sdk-starter-kit
    SHELL
  4. Install dependencies:

    npm install
    SHELL
  5. Run the example:

    npm run start positioning_elements
    SHELL
  6. Click the Preview URL link shown in the terminal to open the example in the Canva editor.

Example app source code

// For usage information, see the README.md file.
/* eslint-disable no-restricted-imports */
import {
Alert,
Box,
Button,
FormField,
Grid,
ImageCard,
Rows,
Select,
Text,
} from "@canva/app-ui-kit";
import type { AppElementOptions, Placement } from "@canva/design";
import {
addElementAtPoint,
getCurrentPageContext,
initAppElement,
} from "@canva/design";
import cat from "assets/images/cat.jpg";
import dog from "assets/images/dog.jpg";
import rabbit from "assets/images/rabbit.jpg";
import { useCallback, useEffect, useState } from "react";
import * as styles from "styles/components.css";
import { upload } from "@canva/asset";
import { useFeatureSupport } from "utils/use_feature_support";
// Below values are only for demonstration purposes.0
// You can position your elements anywhere on the page by providing arbitrary
// values for placement attributes: top, left, width, height and rotation.
const enum ElementPlacement {
DEFAULT = "default",
TOP_LEFT = "top_left",
TOP_RIGHT = "top_right",
BOTTOM_LEFT = "bottom_left",
BOTTOM_RIGHT = "bottom_right",
}
// We can't store the image's data URL in the app element's data, since it
// exceeds the 5kb limit. We can, however, store an ID that references the
// image.
type AppElementData = {
imageId: string;
};
type UIState = {
placement?: ElementPlacement;
data: AppElementData;
update?: (opts: AppElementOptions<AppElementData>) => Promise<void>;
};
const images = {
dog: {
title: "Dog",
imageSrc: dog,
imageRef: undefined,
},
cat: {
title: "Cat",
imageSrc: cat,
imageRef: undefined,
},
rabbit: {
title: "Rabbit",
imageSrc: rabbit,
imageRef: undefined,
},
};
const initialState: UIState = {
data: {
imageId: "dog",
},
placement: ElementPlacement.DEFAULT,
};
const appElementClient = initAppElement<AppElementData>({
render: (data) => {
return [
{
type: "image",
ref: images[data.imageId].imageRef,
top: 0,
left: 0,
width: 400,
height: 400,
altText: {
text: `photo of a ${images[data.imageId].title}`,
decorative: undefined,
},
},
];
},
});
export const App = () => {
const isSupported = useFeatureSupport();
const isRequiredFeatureSupported = isSupported(
addElementAtPoint,
getCurrentPageContext,
);
const [state, setState] = useState<UIState>(initialState);
const {
data: { imageId },
} = state;
const disabled = !imageId || imageId.trim().length < 1;
const getPlacement = async (
placement?: ElementPlacement,
): Promise<Placement | undefined> => {
const pageContext = await getCurrentPageContext();
const pageDimensions = pageContext.dimensions;
if (!pageDimensions) {
// Current doctype doesn't support absolute positioning
return;
}
const elementSize =
Math.min(pageDimensions.height, pageDimensions.width) / 2;
switch (placement) {
case ElementPlacement.TOP_LEFT:
return {
top: 0,
left: 0,
width: elementSize,
height: elementSize,
rotation: 0,
};
case ElementPlacement.TOP_RIGHT:
return {
top: 0,
left: pageDimensions.width - elementSize,
width: elementSize,
height: elementSize,
rotation: 0,
};
case ElementPlacement.BOTTOM_LEFT:
return {
top: pageDimensions.height - elementSize,
left: 0,
width: elementSize,
height: elementSize,
rotation: 0,
};
case ElementPlacement.BOTTOM_RIGHT:
return {
top: pageDimensions.height - elementSize,
left: pageDimensions.width - elementSize,
width: elementSize,
height: elementSize,
rotation: 0,
};
default:
return undefined;
}
};
const items = Object.entries(images).map(([key, value]) => {
const { title, imageSrc } = value;
return {
key,
title,
imageSrc,
active: imageId === key,
onClick: () => {
setState((prevState) => {
return {
...prevState,
data: {
...prevState.data,
imageId: key,
},
};
});
},
};
});
const addOrUpdateAppImage = useCallback(async () => {
if (!images[state.data.imageId].imageRef) {
// Upload local image
const { ref } = await upload({
type: "image",
mimeType: "image/jpeg",
url: images[state.data.imageId].imageSrc,
thumbnailUrl: images[state.data.imageId].imageSrc,
width: 400,
height: 400,
aiDisclosure: "none",
});
images[state.data.imageId].imageRef = ref;
}
const placement = await getPlacement(state.placement);
if (state.update) {
state.update({ data: state.data, placement });
} else {
appElementClient.addElement({ data: state.data, placement });
}
}, [state]);
const addImage = useCallback(async () => {
if (!images[state.data.imageId].imageRef) {
// Upload local image
const { ref } = await upload({
type: "image",
mimeType: "image/jpeg",
url: images[state.data.imageId].imageSrc,
thumbnailUrl: images[state.data.imageId].imageSrc,
width: 400,
height: 400,
aiDisclosure: "none",
});
images[state.data.imageId].imageRef = ref;
}
const placement = await getPlacement(state.placement);
await addElementAtPoint({
type: "image",
ref: images[state.data.imageId].imageRef,
altText: {
text: `photo of a ${images[state.data.imageId].title}`,
decorative: undefined,
},
...placement,
});
}, [state]);
useEffect(() => {
appElementClient.registerOnElementChange((appElement) => {
setState((prevState) => {
return appElement
? {
...prevState,
data: {
...prevState.data,
...appElement.data,
},
update: appElement.update,
}
: { ...prevState, update: undefined };
});
});
}, []);
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Text>
This example demonstrates how apps can get the dimensions of the
current page and create elements at specific positions on that page.
</Text>
<FormField
label="Select an image"
control={({ id }) => (
<Box id={id} padding="1u">
<Grid columns={3} spacing="1.5u">
{items.map((item) => (
<ImageCard
ariaLabel="Add image to design"
alt={item.title}
key={item.key}
thumbnailUrl={item.imageSrc}
onClick={item.onClick}
selectable={true}
selected={item.active}
borderRadius="standard"
/>
))}
</Grid>
</Box>
)}
/>
<FormField
label="Placement"
value={state.placement}
control={(props) => (
<Select
{...props}
options={[
{ value: ElementPlacement.DEFAULT, label: "Default" },
{ value: ElementPlacement.TOP_LEFT, label: "Top Left" },
{ value: ElementPlacement.TOP_RIGHT, label: "Top Right" },
{ value: ElementPlacement.BOTTOM_LEFT, label: "Bottom Left" },
{ value: ElementPlacement.BOTTOM_RIGHT, label: "Bottom Right" },
]}
onChange={(event) => {
setState((prevState) => {
return {
...prevState,
placement: event,
};
});
}}
stretch
/>
)}
/>
<Button
variant="secondary"
onClick={addOrUpdateAppImage}
// Positioning appElement absolutely is not supported in certain design types such as docs.
disabled={disabled || !isRequiredFeatureSupported}
>
{state.update ? "Update app element" : "Add app element"}
</Button>
<Button
variant="secondary"
onClick={addImage}
// Positioning elements absolutely is not supported in certain design types such as docs.
disabled={disabled || !isRequiredFeatureSupported}
>
Add element
</Button>
{!isRequiredFeatureSupported && <UnsupportedAlert />}
</Rows>
</div>
);
};
const UnsupportedAlert = () => (
<Alert tone="warn">
Sorry, the required features are not supported in the current design.
</Alert>
);
TYPESCRIPT
import { AppUiProvider } from "@canva/app-ui-kit";
import { createRoot } from "react-dom/client";
import { App } from "./app";
import "@canva/app-ui-kit/styles.css";
const root = createRoot(document.getElementById("root") as Element);
function render() {
root.render(
<AppUiProvider>
<App />
</AppUiProvider>,
);
}
render();
if (module.hot) {
module.hot.accept("./app", render);
}
TYPESCRIPT
# Positioning elements
Demonstrates how to position elements at specific coordinates and placements within designs. Shows element positioning patterns, placement configurations, and coordinate-based element insertion.
For API reference docs and instructions on running this example, see: https://www.canva.dev/docs/apps/examples/positioning-elements/.
Related examples: See design_interaction/design_editing for advanced element manipulation, or app_element_children for relative positioning within app elements.
NOTE: This example differs from what is expected for public apps to pass a Canva review:
- Static assets are used for demonstration purposes only. Production apps should host assets on a CDN/hosting service and use the `upload` function from the `@canva/asset` package
- ESLint rule `no-restricted-imports` is disabled for example purposes only. Production apps should not disable linting rules without proper justification
- Positioning coordinate validation is simplified for demonstration. Production apps should validate positioning coordinates and handle different page types appropriately
- Error handling is simplified for demonstration. Production apps must implement comprehensive error handling with clear user feedback and graceful failure modes
- Internationalization is not implemented. Production apps must support multiple languages using the `@canva/app-i18n-kit` package to pass Canva review requirements
MARKDOWN

API Reference

Need Help?