When developing a custom widget for ArcGIS Experience Builder, creating an engaging and functional user interface (UI) is essential. This topic will walk you through the process of building the UI for your widget using JSX, web components, and various styling techniques.
Writing JSX
ArcGIS Experience Builder widgets are React components. UI is declared with JSX, a syntax extension that lets you express component trees using HTML-like markup embedded in JavaScript. JSX compiles to React element creation and supports expressions, props, composition, and conditional rendering.
Experience Builder supports both class and function components. You develop widgets using:
- Class components if you prefer lifecycle methods or PureComponent semantics.
- Function components to use React hooks for state and side effects.
Class components
A class component extends React. (or React.) and implements render() to return JSX.
// in widget.tsx:
import { React, AllWidgetProps } from 'jimu-core';
export default class Widget extends React.PureComponent<AllWidgetProps<{}>, unknown> {
render() {
return (
<div className="myWidget">
<p>This is a sample widget</p>
<button type="button" style={{ background: 'orange' }}>I'm a button</button>
</div>
);
}
}Function components
A function component is a plain function that returns JSX. It is the preferred style for most widgets and works seamlessly with React hooks.
// in widget.tsx:
import { React, AllWidgetProps } from 'jimu-core';
export default function Widget(props: AllWidgetProps<{}>) {
return (
<div className="myWidget">
<p>This is a sample widget</p>
<button type="button" style={{ background: 'orange' }}>I'm a button</button>
</div>
);
}Function components can use hooks (for example, use and use) for local state and side effects.
Output:
Using Jimu UI library
The Jimu framework provides a UI library of components for developers to use in their widget development, such as:
- Basic UI components: button, dropdown, inputs, icon, nav, modal, paper, etc.
- Advanced UI components: date picker, resource selector, etc.
You can preview most of the commonly used components and icons by visiting the Storybook site:
https.
Read more about Storybook in Experience Builder.
Jimu UI is the official UI library for Experience Builder, and it is highly recommended that the components from this library are considered and utilized in your UI development. The reasons are as follows:
- UI/UX consistency: the overall look and feel of the widgets, as well as the apps created with them, follows a consistent pattern.
- Themeable: the styles of components are configurable and themeable, which makes it easier to make your widgets compatible with different themes.
- Better integration with Experience Builder and ArcGIS.
Import components
The basic UI components can be directly imported from 'jimu-ui', and the advanced UI components need to be imported separately by using their paths:
import { Button, Icon, Paper, TextInput } from 'jimu-ui'; // basic
import { DatePicker } from 'jimu-ui/date-picker'; // advancedCode example
Here we add a Button component with the "primary" style and an extra star icon to the widget:
// in widget.tsx:
import { React, AllWidgetProps } from 'jimu-core';
import { Button, Icon } from 'jimu-ui'; // import components
import { StarFilled } from 'jimu-icons/filled/application/star'
// Create an svg icon using Icon component:
const iconNode = <StarFilled />;
export default class Widget extends React.PureComponent<AllWidgetProps, any>{
render(){
// Add Button component containing an icon to the widget:
return <Button type="primary">{iconNode} primary button</Button>;
}
}Output:
Using Paper component as a widget container
To maintain consistent widget styling, the widget container should preferably use the Paper component.
const Widget = () => {
return <Paper variant="flat" shape="none" className="jimu-widget widget-xxx">...</Paper>
}Default styles:
- Background:
theme.sys.color.surface.paper - Text:
theme.sys.color.surface.paperText
For lighter text, use surface.paper, preferably via <Typography color="paper:
<Typography color="paperHint">Secondary text</Typography>Other options:
- Border:
<Paper variant="outlined" /> - BorderRadius:
<Paper shape="shape1" /(default is> shape2) - Transparent background:
<Paper variant="flat" transparent />
Using Calcite components
Jimu UI is the primary component library for Experience Builder, and you may want to use that first because it will keep your custom widgets and themes consistent with the rest of the Experience Builder theme. If your widget requires Calcite Design System components, see the Calcite sample widget. Import components from calcite-components (not @esri/calcite-components).
Using ArcGIS Maps SDK for JavaScript components
You can add ArcGIS Maps SDK for JavaScript components to your widget. These web components, such as <arcgis-map, provide mapping functionality as reusable custom elements. They are framework-agnostic and can be used with React, plain JavaScript, or most module bundlers, making it easy to include mapping UI with minimal setup. If you need help getting started, see the ArcGIS Maps SDK sample widget.
Style your widget
In Experience Builder, there are some options to style a widget:
CSS-in-JS (recommended)
CSS-in-JS refers to a way of writing CSS in JavaScript to tackle issues that CSS does not solve, such as vendor prefixes, scoped CSS, JS logics, theming capability, etc.
There are many popular CSS-in-JS libraries out there, such as Styled Components and Emotion. In Experience Builder, we use Emotion as the framework for styling and theming purposes.
Emotion provides two styling patterns:
1. css prop
The css prop from Emotion allows you to write CSS styles in a more natural and friendly way compared to React's style prop.
CSS styles can be written in template literals, which allows you to write JS logics inside of CSS.
For example, the following sample Counter widget changes its text color from red to green when the count value is greater than 2:
// widget.tsx:
import { React, css, type AllWidgetProps } from 'jimu-core';
import { Button, ButtonGroup } from 'jimu-ui';
interface State {
count: number;
}
export default class Widget extends React.PureComponent<AllWidgetProps<{}>, State>{
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
render() {
const numberStyle = css`
font-size: 2.5rem;
color: ${this.state.count > 2 ? 'green' : 'red'};
`;
return <div className="text-center">
<p css={numberStyle}>{this.state.count}</p>
<ButtonGroup variant="outlined" color="primary">
<Button onClick={e => {setCount(count - 1)}}> - </Button>
<Button onClick={e => {setCount(count + 1)}}> + </Button>
</ButtonGroup>
</div>;
}
}or
// widget.tsx:
import { React, css, type AllWidgetProps } from 'jimu-core';
import { Button, ButtonGroup } from 'jimu-ui';
const Widget = (props: AllWidgetProps<{}>): React.ReactElement => {
const [count, setCount] = React.useState(0);
const numberStyle = css`
font-size: 2.5rem;
color: ${count > 2 ? 'green' : 'red'};
`;
return <div className="text-center">
<p css={numberStyle}>{count}</p>
<ButtonGroup variant="outlined" color="primary">
<Button onClick={e => {setCount(count - 1)}}> - </Button>
<Button onClick={e => {setCount(count + 1)}}> + </Button>
</ButtonGroup>
</div>;
}Output:
2. Styled components
This pattern is inspired by the styled-components library and the usage is very similar. The "styled" approach is perfect for creating re-usable components within your widget.
import { React, AllWidgetProps } from 'jimu-core';
import { styled } from 'jimu-theme';
// A styled button component:
const StyledButton = styled.button`
color: white;
background-color: blue;
transition: 0.15s ease-in all;
&:hover {
background-color: darkblue;
}
`;
export default class Widget extends React.PureComponent<AllWidgetProps<{}>>{
render() {
return <StyledButton>
A styled HTML Button
</StyledButton>;
}
}Output:
CSS utility classes:
Jimu UI provides the same CSS utility classes to quickly apply styles to UI elements.
Quick sample:
Here we add w-100, p-3, bg-paper, text-paper, border to make an element:
- take 100% width of its parent
- have paddings of 12px
- use paper color from the theme as its background color
- use paper text color from the theme as its text color
- use divider primary color from the theme as its border color
// in the render() function:
return <div className="w-100 p-3 bg-paper text-paper border">
<p>This is a sample widget</p>
</div>;Output:
Inline CSS
In the context of React, inline CSS styles are written as JavaScript objects and
applied to the style attribute of a DOM element, such as:
// in the render() function:
const containerStyle = {
background: 'darkblue',
color: 'white',
width: 200,
height: 150,
padding: '1rem',
borderRadius: 5
};
return <div
style={containerStyle} // CSS styles applied
> content </div>;Output:
External CSS style sheets
Another way is to define CSS styles in external stylesheet files and import them separately in the widget.
Accepted stylesheet file extensions are: .css, .sass, and .scss.
Take the previous code sample as an example; we can move the CSS styles into a separate stylesheet (e.g., style.css):
/* style.css */
.my-widget {
background: 'darkblue';
color: 'white';
width: 200px;
height: 150px;
padding: '1rem';
border-radius: 5px;
}then import the file in your widget as:
// widget.tsx:
import 'path/to/style.css';and don't forget to add the class name to the DOM element defined in the style.css:
// widget.tsx:
return <div className="my-widget"> content </div>;Output:
Working with theme
This is required if you would like your widget to look consistent with the rest of the application and if you would like to update the look and feel automatically when the theme changes.
Accessing theme variables from widget props
Under the hood, Experience Builder framework provides the theme variables as a JSON object and injects it into the widgets as a property. You have access to all theme variables, such as color, typography, shadow, etc.
Use props.theme to access the theme variables within your widget and reference them in your CSS declaration. For example:
import { React, AllWidgetProps } from 'jimu-core';
import { css } from 'jimu-core';
export default class Widget extends React.PureComponent<AllWidgetProps<{}>>{
render() {
const theme = this.props.theme;
const style = css({
color: theme.sys.color.surface.paperText,
backgroundColor: theme.sys.color.surface.paper,
padding: theme.sys.spacing(3),
borderRadius: theme.sys.shape.shape2
});
return <div css={style}>
<p>This is a sample widget</p>
</div>;
}
}or
import { React, css, AllWidgetProps } from 'jimu-core';
export default function Widget(props: AllWidgetProps<{}>) {
const { theme } = props;
const style = css({
color: theme.sys.color.surface.paperText,
backgroundColor: theme.sys.color.surface.paper,
padding: theme.sys.spacing(3),
borderRadius: theme.sys.shape.shape2
});
return <div css={style}>
<p>This is a sample widget</p>
</div>;
}Output:
Accessing theme variables in components
For more complex UIs inside a widget, you can further break them down into components that use the theme:
Accessing theme variables via styled component (recommended)
// my-component.tsx
import { React } from 'jimu-core';
import { styled } from 'jimu-theme'
const MyComponent = styled('div')(({ theme }) => ({
color: theme.sys.color.surface.paperText,
backgroundColor: theme.sys.color.surface.paper,
padding: theme.sys.spacing(3),
borderRadius: theme.sys.shape.shape2
}));
export default MyComponent;
//widget.tsx
import { React, AllWidgetProps } from 'jimu-core';
import MyComponent from './my-component';
export default function Widget(props: AllWidgetProps<{}>) {
return (
<MyComponent>
<p>This is a sample widget</p>
</MyComponent>
);
}
Accessing theme variables via hook
// my-component.tsx
import { React, css } from 'jimu-core';
import { useTheme } from 'jimu-theme'
const MyComponent = ({ children }) => {
const theme = useTheme();
const style = css({
color: theme.sys.color.surface.paperText,
backgroundColor: theme.sys.color.surface.paper,
padding: theme.sys.spacing(3),
borderRadius: theme.sys.shape.shape2
});
return <div css={style}>
{children}
</div>;
};
export default MyComponent;
//widget.tsx
import { React, AllWidgetProps } from 'jimu-core';
import MyComponent from './my-component';
export default function Widget(props: AllWidgetProps<{}>) {
return <MyComponent>
<p>This is a sample widget</p>
</MyComponent>;
}
Accessing theme variables via HOC with Theme
// my-component.tsx
import { React, css } from 'jimu-core';
import { withTheme } from 'jimu-theme'
const MyComponent = ({ theme, children }) => {
const style = css({
color: theme.sys.color.surface.paperText,
backgroundColor: theme.sys.color.surface.paper,
padding: theme.sys.spacing(3),
borderRadius: theme.sys.shape.shape2
});
return <div css={style}>
{children}
</div>
};
export default withTheme(MyComponent)
//widget.tsx
import { React, AllWidgetProps } from 'jimu-core';
import MyComponent from './my-component';
export default function Widget(props: AllWidgetProps<{}>) {
return <MyComponent>
<p>This is a sample widget</p>
</MyComponent>;
}
ArcGIS Maps SDK components
The framework has handled the theme token mapping between the Jimu theme and Calcite theme, this means you don't need to care about the style of the Calcite components. However, if you don't use the Calcite components and Jimu components, for example, if you use a ArcGIS Maps SDK web component, you may need to override the tokens in your widget if it does not fit the theme.