On September 25th, 2024, we released v2 of the Apps SDK. To learn what’s new and how to upgrade, see Migration FAQ and Migration guide.

Creating app elements

How to create app elements.

Once an app adds elements to a user's design, such as images or videos, the app can't edit those elements — it's as if they become invisible to the app.

Sometimes, this can be limiting.

For example, imagine an app that creates gradients. If the app creates the gradients as images, the user can't change the gradient once it exists. They can only create new gradients. To update a gradient:

  • The user has to delete the previous gradient from their design.
  • A new image has to be uploaded to the user's media library.

This is a sub-par user experience that app elements are designed to solve.

What are app elements?

App elements are a type of element that apps can modify after the element exists in the user's design. They have limitations and a more complex lifecycle, so it doesn't always make sense to use them, but if you're otherwise unable to create the app you want, app elements may be the answer.

Behind the scenes, app elements are groups of elements that:

  • Can't be un-grouped — that is, they're locked groups
  • Can have metadata attached to them

Like groups, app elements can be made up of multiple child elements, but unlike groups, app elements are allowed to contain a single element.

By attaching metadata to an element, the element's settings — for example, the colors of a gradient — can be persisted on the element itself. The app can update the metadata, causing the element to re-render. The end result is that the elements can be edited by the app that created them.

How to create app elements

Step 1: Enable the required permissions

In the Developer Portal, enable the following permissions:

  • canva:design:content:read
  • canva:design:content:write

In the future, the Apps SDK will throw an error if the required permissions are not enabled.

To learn more, see Configuring permissions.

Step 2: Define the app element's data structure

Create a type that represents data required to render the element. For example, for an app element to render a gradient, an appropriate type would need to hold at least two colors:

type AppElementData = {
color1: string;
color2: string;
};
TS

The name of the type is not important.

Step 3: Initialize the app element

Import the initAppElement method from the @canva/design package:

import { initAppElement } from "@canva/design";
TS

Then call the method outside of a React component:

const appElementClient = initAppElement<AppElementData>({
render: (data) => {
const dataUrl = createGradient(data.color1, data.color2);
return [
{
type: "image",
dataUrl,
width: 640,
height: 360,
top: 0,
left: 0,
altText: {
text: "A gradient background",
decorative: false
},
},
];
},
});
TS

There's a few things going on here, so to break it down:

  • The initAppElement method should be called outside of a React component because the rendering of the element is not tied to the rendering of the component.
  • The type for the app element data is passed to the initAppElement method as a type argument. This ensures accurate type information while working with the method.
  • The initAppElement method accepts an object as its only parameter. This object requires a render function that determines the elements to render in the app element. It receives the app element data as its only argument and must return one or more elements.
  • The returned elements must have positional properties, including coordinates and dimensions. To learn more about these options, see Positioning elements.
  • The render method should only rely on data that's passed in through the data parameter. Given the same data, it should return the same result.

In this particular example, the data is passed into the following createGradient function that returns a data URL for a gradient. Add the following function after the React component to create a gradient image:

function createGradient(color1: string, color2: string): string {
const canvas = document.createElement("canvas");
canvas.width = 640;
canvas.height = 360;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Can't get CanvasRenderingContext2D");
}
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, color1);
gradient.addColorStop(1, color2);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return canvas.toDataURL();
}
TS

Step 4: Add the app element's data

To add an app element, start by creating a variable for the app element state within the React component:

appElementClient.addOrUpdateElement({
color1: "",
color2: "",
});
TS

This method accepts an object that conforms to the structure of the app element's data.

If an app element isn't selected, an app element is created and added to the user's design. If an app element is selected, its data updates and the element is re-rendered.

The following code demonstrates how an app might allow the user to customize the values and then render the app element when the user clicks a button:

export function App() {
const [state, setState] = React.useState({
color1: "",
color2: "",
});
function handleClick() {
appElementClient.addOrUpdateElement({
color1: state.color1,
color2: state.color2,
});
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setState((prevState) => {
return {
...prevState,
[event.target.name]: event.target.value,
};
});
}
return (
<div>
<div>
<input
type="text"
name="color1"
value={state.color1}
placeholder="Color #1"
onChange={handleChange}
/>
</div>
<div>
<input
type="text"
name="color2"
value={state.color2}
placeholder="Color #2"
onChange={handleChange}
/>
</div>
<button type="submit" onClick={handleClick}>
Add or update element
</button>
</div>
);
}
TS

The maximum amount of data that can be attached to an app element is 5kb.

Step 5: Update a selected app element

In the previous code sample, there's the Add or update element button:

<button type="submit" onClick={handleClick}>
Add or update element
</button>
TSX

This isn't the ideal user experience. It'd be nicer if we showed an Add element button when an element isn't selected and an Update element button when an element is selected.

To do this, create an isSelected property in the useState hook:

const [state, setState] = React.useState({
color1: "",
color2: "",
isSelected: false,
});
TS

Then, in a useEffect hook, register a callback with the registerOnElementChange method:

React.useEffect(() => {
appElementClient.registerOnElementChange((element) => {
console.log(element);
});
}, []);
TS

This callback runs when:

  • A user selects an app element.
  • A user changes or updates an app element's data.
  • A user deselects an app element.

The callback receives an element parameter. When an element is selected, element contains a value. Otherwise, it's undefined. You can use this behavior to update the isSelected state:

React.useEffect(() => {
appElementClient.registerOnElementChange((element) => {
if (element) {
setState({
isSelected: true,
});
} else {
setState({
isSelected: false,
});
}
});
}, []);
TS

You can then update the UI based on the update state:

<button type="submit" onClick={handleClick}>
{state.isSelected ? "Update element" : "Add element"}
</button>
TSX

Step 6: Synchronize the UI

There's a problem with these text fields:

<div>
<div>
<input
type="text"
name="color1"
value={state.color1}
placeholder="Color #1"
onChange={handleChange}
/>
</div>
<div>
<input
type="text"
name="color2"
value={state.color2}
placeholder="Color #2"
onChange={handleChange}
/>
</div>
<button type="submit" onClick={handleClick}>
{state.isSelected ? "Update element" : "Add element"}
</button>
</div>
TSX

These fields always show the values of the previously selected app element. This is because the state of the fields only update in response to input change events — not in response to the user's selection. As a result, the UI falls out of sync.

Here's what should happen:

  • If you select an app element, the fields should reflect the colors of the selected element.
  • If you deselect an app element, the fields should reset to an empty string.

To fix this, use the registerOnElementChange callback to update the state of the fields:

React.useEffect(() => {
appElementClient.registerOnElementChange((element) => {
if (element) {
setState({
color1: element.data.color1,
color2: element.data.color2,
isSelected: true,
});
} else {
setState({
color1: "",
color2: "",
isSelected: false,
});
}
});
}, []);
TS

The element parameter is an object that contains the app element's data. When an app element is selected, we can use this data to set the values of the text fields. If element is undefined, we can reset the text fields to empty strings. As a result, the UI remains in sync with the user's selection.

How to create app elements (beta)

This API is in preview mode and may experience breaking changes. Apps that use this API will not pass the review process and can't be made available on the Apps Marketplace.

We have introduced a new approach for creating and updating elements in the beta version of the initAppElement API.

The new addElement method and update function solves the problem of inconsistent app element updates. When you select an app element, and update the metadata, if you select another app element before the update is complete, the metadata update could unintentionally apply to the newly selected app element. If no selection is made, it could unintentionally create a new app element.

This is a sub-par experience that the beta version of the initAppElement API solves.

You can access preview methods and functions using the beta dist-tag, and test the beta version of the initAppElement API by modifying the gradient example in the previous section:

  1. Install the beta version of the @canva/design package:

    npm install @canva/design@beta
    SHELL
  2. Import the AppElementOptions type:

    import { AppElementOptions, initAppElement } from "@canva/design";
    TSX
  3. Create a type outside of the React component that:

    • Represents the data that can be stored on the app element
    • Includes a function to handle app element updates

    For example:

    type AppElementChangeEvent = {
    data: AppElementData;
    update?: (opts: AppElementOptions<AppElementData>) => Promise<void>;
    };
    TSX
  4. Update the state variable to conform to the AppElementChangeEvent type:

    export function App() {
    const [state, setState] = React.useState<AppElementChangeEvent>({
    data: {
    color1: "",
    color2: "",
    },
    });
    }
    TSX
  5. Modify the useEffect hook to use the data object:

    React.useEffect(() => {
    appElementClient.registerOnElementChange((element) => {
    if (element) {
    setState({
    data: {
    color1: element.data.color1,
    color2: element.data.color2,
    },
    update: element.update,
    });
    } else {
    setState({
    data: {
    color1: "",
    color2: "",
    },
    });
    }
    });
    }, []);
    TSX
  6. Modify the handleClick function to use the addElement method and the data object:

    function handleClick() {
    if (state.update) {
    state.update({
    data: state.data,
    });
    } else {
    appElementClient.addElement({
    data: state.data,
    });
    }
    }
    TSX
  7. Modify the handleChange function to use the data object:

    function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
    setState((prevState) => {
    return {
    ...prevState,
    data: {
    ...prevState.data,
    [event.target.name]: event.target.value,
    },
    };
    });
    }
    TS
  8. Adjust the input values and Add element button state to use the data object and update function:

    return (
    <div>
    <div>
    <input
    type="text"
    name="color1"
    value={state.data.color1}
    placeholder="Color #1"
    onChange={handleChange}
    />
    </div>
    <div>
    <input
    type="text"
    name="color2"
    value={state.data.color2}
    placeholder="Color #2"
    onChange={handleChange}
    />
    </div>
    <button type="submit" onClick={handleClick}>
    {state.update ? "Update" : "Add"}
    </button>
    </div>
    );
    TSX

Known limitations

  • Users can't apply effects to app elements or the elements within them.
  • Users can't select the individual elements within an app element.
  • Users can't un-group the elements within an app element.
  • App elements can only be edited via the apps that created them.
  • App elements can't contain groups, tables, videos, or other app elements.
  • Apps can only edit app elements while they're selected.
  • The maximum amount of data that can be attached to an app element is 5kb.

API reference

Code sample

import React from "react";
import { initAppElement } from "@canva/design";
type AppElementData = {
color1: string;
color2: string;
};
const appElementClient = initAppElement<AppElementData>({
render: (data) => {
const dataUrl = createGradient(data.color1, data.color2);
return [
{
type: "image",
dataUrl,
width: 640,
height: 360,
top: 0,
left: 0,
altText: {
text: "A gradient background",
decorative: false
},
},
];
},
});
export function App() {
const [state, setState] = React.useState({
color1: "",
color2: "",
isSelected: false,
});
React.useEffect(() => {
appElementClient.registerOnElementChange((element) => {
if (element) {
setState({
color1: element.data.color1,
color2: element.data.color2,
isSelected: true,
});
} else {
setState({
color1: "",
color2: "",
isSelected: false,
});
}
});
}, []);
function handleClick() {
appElementClient.addOrUpdateElement({
color1: state.color1,
color2: state.color2,
});
}
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setState((prevState) => {
return {
...prevState,
[event.target.name]: event.target.value,
};
});
}
return (
<div>
<div>
<input
type="text"
name="color1"
value={state.color1}
placeholder="Color #1"
onChange={handleChange}
/>
</div>
<div>
<input
type="text"
name="color2"
value={state.color2}
placeholder="Color #2"
onChange={handleChange}
/>
</div>
<button type="submit" onClick={handleClick}>
{state.isSelected ? "Update" : "Add"}
</button>
</div>
);
}
function createGradient(color1: string, color2: string): string {
const canvas = document.createElement("canvas");
canvas.width = 640;
canvas.height = 360;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Can't get CanvasRenderingContext2D");
}
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, color1);
gradient.addColorStop(1, color2);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return canvas.toDataURL();
}
TSX
import React from "react";
import { AppElementOptions, initAppElement } from "@canva/design";
type AppElementData = {
color1: string;
color2: string;
};
type AppElementChangeEvent = {
data: AppElementData;
update?: (opts: AppElementOptions<AppElementData>) => Promise<void>;
}
const appElementClient = initAppElement<AppElementData>({
render: (data) => {
const dataUrl = createGradient(data.color1, data.color2);
return [
{
type: "image",
dataUrl,
width: 640,
height: 360,
top: 0,
left: 0,
},
];
},
});
export function App() {
const [state, setState] = React.useState<AppElementChangeEvent>({
data: {
color1: "",
color2: "",
},
});
React.useEffect(() => {
appElementClient.registerOnElementChange((element) => {
if (element) {
setState({
data: {
color1: element.data.color1,
color2: element.data.color2,
},
update: element.update,
});
} else {
setState({
data: {
color1: "",
color2: "",
},
});
}
});
}, []);
function handleClick() {
if (state.update) {
state.update({
data: state.data,
});
} else {
appElementClient.addElement({
data: state.data,
})
}
}
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
setState((prevState) => {
return {
...prevState,
data: {
...prevState.data,
[event.target.name]: event.target.value,
},
};
});
}
return (
<div>
<div>
<input
type="text"
name="color1"
value={state.data.color1}
placeholder="Color #1"
onChange={handleChange}
/>
</div>
<div>
<input
type="text"
name="color2"
value={state.data.color2}
placeholder="Color #2"
onChange={handleChange}
/>
</div>
<button type="submit" onClick={handleClick}>
{state.update ? "Update" : "Add"}
</button>
</div>
);
}
function createGradient(color1: string, color2: string): string {
const canvas = document.createElement("canvas");
canvas.width = 640;
canvas.height = 360;
const ctx = canvas.getContext("2d");
if (!ctx) {
throw new Error("Can't get CanvasRenderingContext2D");
}
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, color1);
gradient.addColorStop(1, color2);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return canvas.toDataURL();
}
TS