Custom UI with JSX
You can display custom user interface (UI) JSX components using the
@metamask/snaps-sdk
module when
implementing the following features:
JSX is supported in MetaMask Extension and Flask version 12+. New UI components will be added as JSX components. The previous function-based library is deprecated.
To use custom UI with JSX, first install @metamask/snaps-sdk
using the following command:
yarn add @metamask/snaps-sdk
Then, whenever you're required to return a custom UI JSX component, import the components from the
SDK at @metamask/snaps-sdk/jsx
and build your UI with them.
For example, to display a Box
(the panel
function equivalent) using snap_dialog
:
import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx";
await snap.request({
method: "snap_dialog",
params: {
type: "alert",
content: (
<Box>
<Heading>Alert heading</Heading>
<Text>Something happened in the system.</Text>
</Box>
),
},
});
Note that JSX can only be used in .jsx
or .tsx
files.
Upgrade an existing Snap to use JSX
Follow these steps to upgrade an existing Snap to use JSX:
-
Upgrade dependencies in
packages/snap/package.json
:- Upgrade
@metamask/snaps-sdk
to^6.1.1
or later. - Upgrade
@metamask/snaps-cli
to^6.2.1
or later. - Upgrade
@metamask/snaps-jest
to^8.2.0
or later.
Run
yarn install
to install the new versions. - Upgrade
-
Update
packages/snap/.eslintrc.js
:- Add a new section in
overrides
with the following configuration:{
"files": ["**/*.ts", "**/*.tsx"],
"extends": ["@metamask/eslint-config-typescript"],
"rules": {
// This allows importing the `Text` JSX component.
"@typescript-eslint/no-shadow": [
"error",
{
"allow": ["Text"],
},
],
},
} - Replace
["*.test.ts"]
with["*.test.ts", "*.test.tsx"]
.
- Add a new section in
-
Update
packages/snap/src/index.ts
, if it will have JSX:- Rename the file to
index.tsx
. - Modify the
input
field inpackages/snap/snap.config.ts
tosrc/index.tsx
.
- Rename the file to
-
Update
packages/snap/tsconfig.json
:- Under
compilerOptions
, add:"jsx": "react-jsx",
"jsxImportSource": "@metamask/snaps-sdk" - Change the
include
property from["**/*.ts"]
to["**/*.ts", "**/*.tsx"]
.
- Under
-
Replace all Custom UI in your code with JSX components, renaming the target files with the
.tsx
extension.
Components
The following custom UI JSX components are available:
Address
Outputs a formatted text field for an Ethereum address. The address is automatically displayed with a jazzicon and truncated value. Hovering the address shows the full value in a tooltip.
Example
import { Box, Heading, Address } from "@metamask/snaps-sdk/jsx";
await snap.request({
method: "snap_dialog",
params: {
type: "alert",
content: (
<Box>
<Heading>Are you sure you want to send tokens to this address?</Heading>
<Address address="0x000000000000000000000000000000000000dEaD" />
</Box>
),
},
});
Bold
Outputs bold text.
Example
import { Box, Heading, Text, Bold } from "@metamask/snaps-sdk/jsx";
await snap.request({
method: "snap_dialog",
params: {
type: "alert",
content: (
<Box>
<Heading>Hello world!</Heading>
<Text>
This is <Bold>bold</Bold>.
</Text>
</Box>
),
},
});
Box
Outputs a box, which can be used as a container for other components.
Props
direction
- (Optional) The direction in which elements flow inside the box. Possible values are"horizontal"
or"vertical"
. The default is"vertical"
.alignment
- (Optional) The alignment of the elements inside the box. Possible values are"start"
,"center"
,"end"
,"space-between"
, or"space-around"
. The default is"start"
.
Example
import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx";
module.exports.onHomePage = async () => {
return {
content: (
<Box>
<Heading>Features</Heading>
<Box
direction="horizontal"
alignment="space-around"
>
<Text>Feature 1</Text>
<Text>Feature 2</Text>
<Text>Feature 3</Text>
</Box>
</Box>
),
};
};
Button
Outputs a button that the user can select. For use in interactive UI.
Props
children
:string
- The text of the button.type
- (Optional) The type of button. Possible values are"button"
or"submit"
. The default is"button"
.name
:string
- (Optional) The name that will be sent toonUserInput
when a user selects the button.variant
- (Optional) Determines the appearance of the button. Possible values are"primary"
or"destructive"
. The default is"primary"
.
Example
import { Box, Heading, Button } from "@metamask/snaps-sdk/jsx";
const interfaceId = await snap.request({
method: "snap_createInterface",
params: {
ui: (
<Box>
<Heading>Interactive interface</Heading>
<Button name="interactive-button">Click me</Button>
</Box>
),
},
});
await snap.request({
method: "snap_dialog",
params: {
type: "Alert",
id: interfaceId,
},
});
Copyable
Outputs a read-only text field with a copy-to-clipboard shortcut.
Props
value
:string
- The value to copy when the user clicks on the copyable element.sensitive
:boolean
- (Optional) Indicates whether the value is sensitive. Iftrue
, the value will be hidden when the user is not interacting with the copyable element.
Example
import { Box, Text, Copyable } from "@metamask/snaps-sdk/jsx";
await snap.request({
method: "snap_dialog",
params: {
type: "alert",
content: (
<Box>
<Text>Your address:</Text>
<Copyable value="0x000000000000000000000000000000000000dEaD" />
</Box>
),
},
});
Divider
Outputs a horizontal divider.
Example
import { Box, Heading, Divider, Text } from "@metamask/snaps-sdk/jsx";
module.exports.onHomePage = async () => {
return {
content: (
<Box>
<Heading>Hello world!</Heading>
<Divider />
<Text>Welcome to my Snap home page!</Text>
</Box>
),
};
};
Dropdown
Outputs a dropdown for use in interactive UI.
Props
name
:string
- The name sent toonUserInput
.children
:Option[]
- One or moreOption
components with the following props:value
:string
- The value sent toonUserInput
.children
:string
- The text displayed in the dropdown for that option.
Example
import { Box, Text, Dropdown } from "@metamask/snaps-sdk/jsx";
const interfaceId = await snap.request({
method: "snap_createInterface",
params: {
ui: (
<Box>
<Text>Pick a currency</Text>
<Dropdown name="currency">
<Option value="ETH">ETH</Option>
<Option value="USD">USD</Option>
</Dropdown>
</Box>
),
},
});
await snap.request({
method: "snap_dialog",
params: {
type: "Alert",
id: interfaceId,
},
});
Field
Outputs a form field, wrapping a Dropdown
or Input
to give it a label and optional error.
Props
label
:string
- The label for the wrapped element.error
:string
- Any error for the wrapped element. Setting this changes the styling of the wrapped element to show that there is an error.children
- TheDropdown
orInput
element to be wrapped.
Example
import { Field, Form, Input, Button } from "@metamask/snaps-sdk/jsx";
const interfaceId = await snap.request({
method: "snap_createInterface",
params: {
ui: (
<Form name="form-to-fill">
<Field label="First Name">
<Input name="firstName" placeholder="Enter your first name" />
</Field>
<Button type="submit">Submit</Button>
</Form>
),
},
});
await snap.request({
method: "snap_dialog",
params: {
type: "Alert",
id: interfaceId,
},
});
Form
Outputs a form for use in interactive UI.
Props
name
:string
- The name that will be sent toonUserInput
when a user interacts with the form.children
:array
- An array ofInput
orButton
components.
Example
import { Form, Input, Button } from "@metamask/snaps-sdk/jsx";
const interfaceId = await snap.request({
method: "snap_createInterface",
params: {
ui: (
<Form name="form-to-fill">
<Input name="user-name" placeholder="Your name" />
<Button type="submit">Submit</Button>
</Form>
),
},
});
await snap.request({
method: "snap_dialog",
params: {
type: "Alert",
id: interfaceId,
},
});
Heading
Outputs a heading.
This is useful for Box
titles.
Example
import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx";
module.exports.onHomePage = async () => {
return {
content: (
<Box>
<Heading>Hello world!</Heading>
<Text>Welcome to my Snap home page!</Text>
</Box>
),
};
};
Image
Outputs an image. This component takes an inline SVG. It does not support remote URLs.
You can import SVG, PNG, and JPEG files using an import statement.
These files are automatically imported as SVG strings, so you can pass them directly to the Image
component.
The SVG is rendered within an <img>
tag, which prevents JavaScript or interaction events from
being supported.
To disable image support, set the features.images
configuration option to false
.
The default is true
.
Props
src
:string
- An inline SVG.alt
:string
- An optional alternative text for the image.
Example
import { Box, Heading, Text, Image } from "@metamask/snaps-sdk/jsx";
import svgIcon from "./path/to/icon.svg";
module.exports.onHomePage = async () => {
return {
content: (
<Box>
<Heading>Hello world!</Heading>
<Text>Welcome to my Snap home page!</Text>
<Image src={svgIcon} />
</Box>
),
};
};
See the @metamask/images-example-snap
package for a full example of implementing images.
Input
Outputs an input component for use in interactive UI.
Props
name
:string
- The name that will be used as a key to the event sent toonUserInput
when the containing form is submitted.type
- (Optional) The type of input. Possible values are"text"
,"number"
, or"password"
. The default is"text"
.placeholder
:string
- (Optional) The text displayed when the input is empty.label
:string
- (Optional) The text displayed alongside the input to label it.value
:string
- (Optional) The default value of the input.
Example
import { Form, Input, Button } from "@metamask/snaps-sdk/jsx";
const interfaceId = await snap.request({
method: "snap_createInterface",
params: {
ui: (
<Form name="form-to-fill">
<Input name="user-name" placeholder="Your name" />
<Button type="submit">Submit</Button>
</Form>
),
},
});
await snap.request({
method: "snap_dialog",
params: {
type: "Alert",
id: interfaceId,
},
});
Italic
Outputs italic text.
Example
import { Box, Heading, Text, Italic } from "@metamask/snaps-sdk/jsx";
await snap.request({
method: "snap_dialog",
params: {
type: "alert",
content: (
<Box>
<Heading>Hello world!</Heading>
<Text>
This is <Italic>italic</Italic>.
</Text>
</Box>
),
},
});
Link
Outputs a clickable link.
Props
href
:string
- The URL to point to. This must be an HTTPS URL.children
:Array<string | Bold | Italic>
- The link text.
Example
import { Box, Heading, Link, Text } from "@metamask/snaps-sdk/jsx";
module.exports.onHomePage = async () => {
return {
content: (
<Box>
<Heading>Hello world!</Heading>
<Text>
Download <Link href="https://metamask.io">MetaMask</Link>.
</Text>
<Text>
Read the MetaMask docs at <Link href="https://docs.metamask.io">MetaMask docs</Link>.
</Text>
</Box>
),
};
};
Row
Outputs a row with a label and value, which can be used for key-value data.
Props
label
:string
- The label of the row.variant
- (Optional) The variant of the label. Possible values are"default"
,"error"
, or"warning"
.children
- The value of the row, which can be aText
,Image
, orAddress
component.
Example
import { Box, Row, Text, Address } from "@metamask/snaps-sdk/jsx";
await snap.request({
method: "snap_dialog",
params: {
type: "alert",
content: (
<Box>
<Row label="Address">
<Address address="0x000000000000000000000000000000000000dEaD" />
</Row>
<Row label="Balance">
<Text>1.78 ETH</Text>
</Row>
</Box>
),
},
});
Spinner
Outputs a loading indicator.
Example
import { Box, Heading, Spinner } from "@metamask/snaps-sdk/jsx";
await snap.request({
method: "snap_dialog",
params: {
type: "alert",
content: (
<Box>
<Heading>Please wait...</Heading>
<Spinner />
</Box>
),
},
});
Text
Outputs text.
Example
import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx";
module.exports.onHomePage = async () => {
return {
content: (
<Box>
<Heading>Hello world!</Heading>
<Text>Welcome to my Snap home page!</Text>
</Box>
),
};
};
Emojis
Text-based components (such as Heading
and Text
) accept emojis.
Example
import { Box, Heading, Text } from "@metamask/snaps-sdk/jsx";
await snap.request({
method: "snap_dialog",
params: {
type: "alert",
content: (
<Box>
<Heading>Hello world!</Heading>
<Text>This is an apple 🍎 and this is an orange 🍊.</Text>
</Box>
),
},
});