Design Modules

Using Customizations is a powerful way to provide custom content and data to nodes in a Figma document. Sometimes, a developer might find themselves wanting to reuse customizations or simply group them into a logical unit. Design Modules serve this purpose by allowing customizations to be defined and stored in a class instead of a @Composable function. The class itself can then be used as its own customization in a @DesignComponent function.

Defining a module class

Module classes are declared with the @DesignModuleClass annotation. The annotation takes no arguments and simply annotates the class so that a customizations() extension function will be generated for the class. Within the class, customizations can be declared in the same way that customizations are declared in a @DesignComponent function. However, due to a bug in KSP, different annotations to specify customizations need to be used when declaring them within a module class vs a function. Here is a table of what annotation to use in a function vs a class:

Function Class
@Design @DesignProperty
@DesignVariant @DesignVariantProperty
@DesignContentTypes @DesignContentTypesProperty
@DesignPreviewContent @DesignPreviewContentProperty

Note that once this bug has been fixed, all of the @Design*Property annotations will be deprecated for at least one release, and then later removed, in favor of the analogous annotations used for functions.

Example:

@DesignModuleClass
class TextModule(
    @DesignProperty(node = "#text") val text: String,
    @DesignProperty(node = "#replace")
    val replaceNode: @Composable (ComponentReplacementContext) -> Unit,
    @DesignVariantProperty(property = "#text-style") val textStyle: TextStyle,
    @DesignContentTypesProperty(nodes = ["#button", "#header"])
    @DesignPreviewContentProperty(
        name = "Buttons",
        nodes = [PreviewNode(3, "#button")]
    )
    val content: ListContent,
)

Nested Modules

Module classes can include other modules. The resulting set of customizations will contain all customizations from the current class as well as all nested module classes. Use the @DesignModuleProperty annotation to achieve this, using the class name directly as the type of the @DesignModuleProperty class property.

Example:

@DesignModuleClass
class TextModuleCombined(
    @DesignProperty(node = "#name") val name: String,
    @DesignModuleProperty val textProperties: TextModule,
)

Adding a Module to a @DesignComponent Function

Module classes by themselves are not useful until used as a parameter in a @DesignComponent function. Use the @DesignModule annotation for parameters in such a function.

Example:

interface ModuleExample {
    @DesignComponent(node = "#stage")
    fun Main(
        @Design(node = "#header") header: String,
        @DesignModule moduleCustomizations: TextModuleCombined,
    )
}

Calling the Generated DesignCompose Function

To call the generated function, each module parameter must be passed an instance of the class. These instances can be constructed normally since they use the developer defined class directly, not a generated one. The generated function will combine any customizations from @Design and @DesignVariant parameters in the function with all the embedded customizations from parameters that are modules.

Example:

ModuleExampleDoc.Main(
    headerText = "Module Example",
    moduleCustomizations = TextModuleCombined(
        name = "Combined Module",
        textProperties = TextModule(
            text = "Hello World",
            replaceNode = { ModuleExample.Button("My Button") },
            textStyle = TextStyle.LargeBold,
            content = {
                ListContentData(count = 3) { index ->
                    ModuleExample.Button("Button $index")
                }
            },
        ),
    )
)