App element children

Build composite app elements from multiple child elements.

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 app_element_children
    SHELL
  6. Click the Preview URL link shown in the terminal to open the example in the Canva editor.

Example app source code

import {
Button,
FormField,
NumberInput,
Rows,
Text,
Title,
} from "@canva/app-ui-kit";
import type {
AppElementRendererOutput,
ShapeElementAtPoint,
AppElementOptions,
} from "@canva/design";
import { initAppElement } from "@canva/design";
import { useEffect, useState } from "react";
import * as styles from "styles/components.css";
// The data that will be attached to the app element
type AppElementData = {
rows: number;
columns: number;
width: number;
height: number;
spacing: number;
rotation: number;
};
// The state of the user interface. In this example,
// we have data representing AppElementData, but it *could* be different.
// We also store an update function that can be used to update the app element.
type AppElementChangeEvent = {
data: AppElementData;
update?: (opts: AppElementOptions<AppElementData>) => Promise<void>;
};
// The default values for the UI components.
const initialState: AppElementChangeEvent = {
data: {
rows: 3,
columns: 3,
width: 100,
height: 100,
spacing: 25,
rotation: 0,
},
};
const appElementClient = initAppElement<AppElementData>({
// This callback runs when the app sets the element's data. It receives
// the data and must respond with an array of elements.
render: (data) => {
const elements: AppElementRendererOutput = [];
// For each row and column, create a shape element. The positions of the
// elements are offset to ensure that none of them overlap.
for (let row = 0; row < data.rows; row++) {
for (let column = 0; column < data.columns; column++) {
const { width, height, spacing, rotation } = data;
const top = row * (height + spacing);
const left = column * (width + spacing);
const element = createSquareShapeElement({
width,
height,
top,
left,
rotation,
});
elements.push(element);
}
}
return elements;
},
});
export const App = () => {
const [state, setState] = useState<AppElementChangeEvent>(initialState);
const {
data: { width, height, rows, columns, spacing, rotation },
} = state;
const disabled = width < 1 || height < 1 || rows < 1 || columns < 1;
// This callback runs when the app element's data is modified or when the
// user selects an app element. In both situations, we can use this callback
// to update the state of the UI to reflect the latest data.
useEffect(() => {
appElementClient.registerOnElementChange((appElement) => {
setState(
appElement
? { data: appElement.data, update: appElement.update }
: initialState,
);
});
}, []);
return (
<div className={styles.scrollContainer}>
<Rows spacing="2u">
<Text>
This example demonstrates how app elements can be made up of one or
more elements, and how those elements can be positioned relatively to
one another.
</Text>
<Title size="small">Grid</Title>
<FormField
label="Rows"
value={rows}
control={(props) => (
<NumberInput
{...props}
min={1}
onChange={(value) => {
setState((prevState) => {
return {
...prevState,
data: {
...prevState.data,
rows: Number(value || 0),
},
};
});
}}
/>
)}
/>
<FormField
label="Columns"
value={columns}
control={(props) => (
<NumberInput
{...props}
min={1}
onChange={(value) => {
setState((prevState) => {
return {
...prevState,
data: {
...prevState.data,
columns: Number(value || 0),
},
};
});
}}
/>
)}
/>
<FormField
label="Spacing"
value={spacing}
control={(props) => (
<NumberInput
{...props}
min={1}
onChange={(value) => {
setState((prevState) => {
return {
...prevState,
data: {
...prevState.data,
spacing: Number(value || 0),
},
};
});
}}
/>
)}
/>
<Title size="small">Squares</Title>
<FormField
label="Width"
value={width}
control={(props) => (
<NumberInput
{...props}
min={1}
onChange={(value) => {
setState((prevState) => {
return {
...prevState,
data: {
...prevState.data,
width: Number(value || 0),
},
};
});
}}
/>
)}
/>
<FormField
label="Height"
value={height}
control={(props) => (
<NumberInput
{...props}
min={1}
onChange={(value) => {
setState((prevState) => {
return {
...prevState,
data: {
...prevState.data,
height: Number(value || 0),
},
};
});
}}
/>
)}
/>
<FormField
label="Rotation"
value={rotation}
control={(props) => (
<NumberInput
{...props}
min={-180}
max={180}
onChange={(value) => {
setState((prevState) => {
return {
...prevState,
data: {
...prevState.data,
rotation: Number(value || 0),
},
};
});
}}
/>
)}
/>
<Button
variant="primary"
stretch
onClick={() => {
// This method attaches the provided data to the app element,
// triggering the `registerRenderAppElement` callback.
if (state.update) {
state.update({ data: state.data });
} else {
appElementClient.addElement({ data: state.data });
}
}}
disabled={disabled}
>
{`${state.update ? "Update" : "Add"} element`}
</Button>
</Rows>
</div>
);
};
const createSquareShapeElement = ({
width,
height,
top,
left,
rotation,
}: {
width: number;
height: number;
top: number;
left: number;
rotation: number;
}): ShapeElementAtPoint => {
return {
type: "shape",
paths: [
{
d: `M 0 0 H ${width} V ${height} H 0 L 0 0`,
fill: {
dropTarget: false,
color: "#ff0099",
},
},
],
viewBox: {
width,
height,
top: 0,
left: 0,
},
width,
height,
rotation,
top,
left,
};
};
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
# App element children
Demonstrates how app elements can contain multiple child elements positioned relative to each other. Creates a customizable grid of square shapes where users can control rows, columns, spacing, and individual element properties.
For API reference docs and instructions on running this example, see: https://www.canva.dev/docs/apps/examples/app-element-children/.
Related example: See app_image_elements for working with single elements within app elements.
**NOTE**: This example differs from what is expected for public apps to pass a Canva review:
- Error handling is simplified for demonstration. Production apps must implement comprehensive error handling with clear user feedback and graceful failure modes
- Accessibility features are not fully implemented. Production apps must meet WCAG 2.0 AA standards with proper keyboard navigation and ARIA labels
- Input validation is minimal for demonstration. Production apps must validate all user inputs and provide clear error messaging
- 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?