Using color selectors

How to prompt users to select a color.

Sometimes, apps want to prompt users to select a color. For example, an app for creating patterns might want to prompt users to select the colors for a pattern. To ensure a consistent user experience, apps can use the Apps SDK to open a built-in color selector flyout and detect the selection of colors.

Our design guidelines help you create a high-quality app that easily passes app review.

  • A familiar user experience that's deeply integrated with Canva.
  • Integrated with Brand Kit, helping teams to remain on-brand.
  • Canva Pro users automatically get access to Pro-only features.

Apps are responsible for determining how a color selector opens, but a common pattern is to open the selector when a user clicks a Swatch component.

The color selector itself appears in a flyout interface that's "pinned" to the element that opened it. The exact position of the flyout is calculated and controlled by Canva.

Users can select document colors, Brand Kit colors, or default colors. If non-breaking features are added to the color selector in the future, they will be automatically available to apps without requiring code changes.

While the color selector is open, an invisible overlay is rendered on top of the app. This overlay blocks interactions with the app, such as clicking or scrolling, until the color selector is closed.

The color selector can be configured with one or more scopes. These scopes determine the user experience. At the moment, "solid" is the only supported scope. When enabled, this scope allows the user to select solid colors.

An example of a scope that will be supported in the future is the "palette" scope. This scope will allow users to select color palettes — that is, combinations of colors.

In the Developer Portal, enable the following permissions:

  • canva:brandkit:read
  • canva:design:content:read

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

To learn more, see Configuring permissions.

An anchor is the element to which the color selector will be "pinned" when it's open. This could be a custom component, but we generally recommend using an App UI Kit component, such as the Swatch component:

import { Rows, Swatch } from "@canva/app-ui-kit";
import * as React from "react";
import styles from "styles/components.css";
export function App() {
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Swatch
fill={["#ff0099"]}
onClick={() => {
// code goes here
}}
/>
</Rows>
</div>
);
}
tsx
  1. Import the openColorSelector method from the @canva/asset package:

    import { openColorSelector } from "@canva/asset";
    tsx
  2. In the onClick handler, get the position and dimensions of the anchor:

    <Swatch
    fill={["#ff0099"]}
    onClick={async (event) => {
    const anchor = event.currentTarget.getBoundingClientRect();
    }}
    />
    tsx
  3. Register a callback with the openColorSelector method, passing in the position and dimensions of the anchor as the first argument, and setting the scope and callback as the second argument:

    <Swatch
    fill={["#ff0099"]}
    onClick={async (event) => {
    const anchor = event.currentTarget.getBoundingClientRect();
    await openColorSelector(anchor, {
    scopes: ["solid"],
    onColorSelect: (event) => {
    console.log(event.selection);
    },
    });
    }}
    />
    tsx

    The callback receives an object as its only argument. This object contains information about the color selection event.

  1. Set up a state variable for tracking the currently selected color:

    const [color, setColor] = React.useState<string>("#ff0099");
    tsx
  2. Pass the color state into the Swatch component and, in the onClick handler, update the state variable:

    <Swatch
    fill={[color]}
    onClick={async (event) => {
    const anchor = event.currentTarget.getBoundingClientRect();
    await openColorSelector(anchor, {
    scopes: ["solid"],
    onColorSelect: (event) => {
    if (event.selection.type === "solid") {
    setColor(event.selection.hexString);
    }
    },
    });
    }}
    />
    tsx

Sometimes, apps need to render multiple selectable colors, such as by rendering multiple Swatch components.

The problem is, when a user opens a color selector, the color that's displayed as selected defaults to the most recently selected color, even if the most recent color was selected via a different selector. This leads to the following bug:

  1. In Color Selector #1, the user selects a color.
  2. In Color Selector #2, the color from Color Selector #1 is display as selected.

To solve this problem, explicitly define the color to display as selected for each color selector:

<Swatch
fill={[color]}
onClick={async (event) => {
const anchor = event.currentTarget.getBoundingClientRect();
await openColorSelector(anchor, {
scopes: ["solid"],
selectedColor: color
? {
type: "solid",
hexString: color,
}
: undefined,
onColorSelect: (event) => {
console.log(event.selection);
},
});
}}
/>
tsx

Here, we're passing the previously selected color into the openColorSelector method via the selectedColor property, or a value of undefined if there isn't a previously selected color.

The selectedColor property is not required if an app only renders one selectable color.

The openColorSelector method returns a Promise that, once resolved, returns a function. This function closes the color selector flyout:

// Open the color selector
const closeColorSelector = await openColorSelector(anchor, {
scopes: ["solid"],
onColorSelect: (event) => {
if (event.selection.type === "solid") {
setColor(event.selection.hexString);
}
},
});
// Close the color selector
closeColorSelector();
tsx

Most apps don't need to call this function. The color selector automatically closes when the user clicks outside of the flyout, and this is usually the best behavior. Even so, this function is sometimes a useful escape hatch.

To use this function, it often makes sense to store the function in a state variable so that it can be called throughout the lifecycle of the app. To store and then call the function from a state variable:

  1. Import the CloseColorSelectorFn type:

    import { CloseColorSelectorFn, openColorSelector } from "@canva/asset";
    tsx
  2. Create a state variable for storing the function:

    const [closeColorSelector, setCloseColorSelector] =
    setState<CloseColorSelectorFn>();
    tsx
  3. Store the function returned by the openColorSelector method:

    const closingFn = await openColorSelector(anchor, {
    scopes: ["solid"],
    onColorSelect: (event) => {
    if (event.selection.type === "solid") {
    setColor(event.selection.hexString);
    }
    },
    });
    setCloseColorSelectorFn(() => closingFn);
    tsx
  4. Elsewhere in the app, call the closeColorSelector method:

    closeColorSelector?.();
    tsx

    Here, we're using the ? symbol because closeColorSelector will be undefined until the color selector is opened for the first time.

import { Rows, Swatch } from "@canva/app-ui-kit";
import { openColorSelector } from "@canva/asset";
import * as React from "react";
import styles from "styles/components.css";
export function App() {
const [color, setColor] = React.useState<string>("#ff0099");
return (
<div className={styles.scrollContainer}>
<Rows spacing="1u">
<Swatch
fill={[color]}
onClick={async (event) => {
const anchor = event.currentTarget.getBoundingClientRect();
await openColorSelector(anchor, {
scopes: ["solid"],
onColorSelect: (event) => {
if (event.selection.type === "solid") {
setColor(event.selection.hexString);
}
},
});
}}
/>
</Rows>
</div>
);
}
tsx