Overview
You will learn
- How to use the
ShellBarcomponent - How to use the
AnalyticalTablecomponent - How to style components
Prerequisites
- Complete the tutorial Integrate Charts and Conditional Rendering.
Steps
Intro
So far, you have built your first Card component. Now to take things further, it’s time to build something more interesting. In this tutorial, you will learn how different components work together by building an analytical dashboard.
To make things easier, first import all the components and enums you will need in this step. Just copy the code below and use it to replace the previous imported UI5 Web Components in MyApp.tsx.
import {
Avatar,
Card,
CardHeader,
Text,
ShellBar,
ShellBarItem,
List,
ListItemStandard,
ListItemCustom,
ProgressIndicator,
FlexBox,
FlexBoxJustifyContent,
FlexBoxWrap,
FlexBoxDirection,
AnalyticalTable,
Icon,
} from "@ui5/webcomponents-react";The ShellBar is the central navigation element in your web application and should therefore be visible on all pages.
Feel free to try out the
ShellBarin the Storybook.
Start with adding the
ShellBarthat has aprimaryTitleabove yourCardcomponent.TypeScript<ShellBar primaryTitle="My App" />Add some more properties.
The logo of the application should be displayed and also a profile picture would be nice.
Use the
logoandprofileprop to achieve this. Thelogoprop accepts either animgtag or theAvatarcomponent, theprofileprop only accepts theAvatarcomponent. First add thelogoprop like this:TypeScript<ShellBar logo={<img src="" alt="Company Logo" />} primaryTitle="My App" />Then pass the
profileprop like this:TypeScript<ShellBar logo={<img src="" alt="Company Logo"/>} profile={<Avatar><img src="" alt="User Avatar" /></Avatar>} primaryTitle="My App" />You can use your own image, use a URL to an image or simply download the images below. For the download option, create a new
assets/directory insidesrc/, download the images into that directory, and then import them inMyApp.tsx.Processed Assets:

assets TypeScriptimport reactLogo from "./assets/reactLogo.png"; import profilePictureExample from "./assets/profilePictureExample.png";TypeScript<ShellBar logo={<img src={reactLogo} alt="Company Logo" />} profile={ <Avatar> <img src={profilePictureExample} alt="User Avatar" /> </Avatar> } primaryTitle="My App" />Add custom items:
By passing a
ShellBarItemaschildyou are able to add custom items to yourShellBar. The element is basically aButtonwith responsive behavior and styling adapted to theShellBar.TypeScript<ShellBar logo={<img src={reactLogo} alt="Company Logo" />} profile={ <Avatar> <img src={profilePictureExample} alt="User Avatar" /> </Avatar> } primaryTitle="My App" > <ShellBarItem icon={activateIcon} text="Activate" /> </ShellBar>That is strange – the
ShellBarItemdoes not show an icon.Every
Iconthat is used in a component has to be imported manually. All available icons can be found here.Add this line to your imports:
TypeScriptimport activateIcon from "@ui5/webcomponents-icons/dist/activate.js";Now your
ShellBarItemshows up on the right side of theShellBar.
ShellBar
Your component should now look like this:
import activateIcon from "@ui5/webcomponents-icons/dist/activate.js";
import lineChartIcon from "@ui5/webcomponents-icons/dist/line-chart.js";
import barChartIcon from "@ui5/webcomponents-icons/dist/horizontal-bar-chart.js";
import { useState } from "react";
import {
Avatar,
Card,
CardHeader,
Text,
ShellBar,
ShellBarItem,
List,
ListItemStandard,
ListItemCustom,
ProgressIndicator,
FlexBox,
FlexBoxJustifyContent,
FlexBoxWrap,
FlexBoxDirection,
AnalyticalTable,
Icon,
} from "@ui5/webcomponents-react";
import { BarChart, LineChart } from "@ui5/webcomponents-react-charts";
import reactLogo from "./assets/reactLogo.png";
import profilePictureExample from "./assets/profilePictureExample.png";
const dataset = [
{
month: "January",
data: 65,
},
{
month: "February",
data: 59,
},
{
month: "March",
data: 80,
},
{
month: "April",
data: 81,
},
{
month: "May",
data: 56,
},
{
month: "June",
data: 55,
},
{
month: "July",
data: 40,
},
];
export function MyApp() {
const [toggleCharts, setToggleCharts] = useState("lineChart");
const [loading, setLoading] = useState(false);
const contentTitle =
toggleCharts === "lineChart" ? "Line Chart" : "Bar Chart";
const switchToChart =
toggleCharts === "lineChart" ? "Bar Chart" : "Line Chart";
const handleHeaderClick = () => {
if (toggleCharts === "lineChart") {
setLoading(true);
setTimeout(() => {
setLoading(false);
setToggleCharts("barChart");
}, 2000);
} else {
setLoading(true);
setTimeout(() => {
setLoading(false);
setToggleCharts("lineChart");
}, 2000);
}
};
return (
<div>
<ShellBar
logo={<img src={reactLogo} alt="Company Logo" />}
profile={
<Avatar>
<img src={profilePictureExample} alt="User Avatar" />
</Avatar>
}
primaryTitle="My App"
>
<ShellBarItem icon={activateIcon} text="Activate" />
</ShellBar>
<Card
header={
<CardHeader
titleText="Prices"
subtitleText={`Click here to switch to ${switchToChart}`}
interactive
avatar={
<Icon
name={
toggleCharts === "lineChart" ? lineChartIcon : barChartIcon
}
accessibleName={contentTitle}
/>
}
onClick={handleHeaderClick}
/>
}
style={{ width: "300px" }}
>
<Text style={{ padding: "var(--sapContent_Space_S)" }}>
{contentTitle}
</Text>
{toggleCharts === "lineChart" ? (
<LineChart
dimensions={[{ accessor: "month" }]}
measures={[{ accessor: "data", label: "Price" }]}
dataset={dataset}
loading={loading}
/>
) : (
<BarChart
dimensions={[{ accessor: "month" }]}
measures={[{ accessor: "data", label: "Price" }]}
dataset={dataset}
loading={loading}
/>
)}
</Card>
</div>
);
}In this step, we want to add a new Card to our application/analytical dashboard, that contains a List.
Add a
Card(right after the first one).TypeScript<Card header={ <CardHeader titleText="Progress" subtitleText="List" avatar={<Icon name={listIcon} />} /> } style={{ width: "300px" }} ></Card>Add the list
Iconto your imports.TypeScriptimport listIcon from "@ui5/webcomponents-icons/dist/list.js";Add the
Listcomponent as child of theCard.TypeScript<List></List>To render elements of the list, use the
ListItemStandardand pass astringas child.TypeScript<List> <ListItemStandard>Activity 1</ListItemStandard> </List>Users should know the status of some activities. Add the
additionalTextprop to theStandardListItem. To visualize the status as neutral, positive or negative, also add theadditionalTextStateprop.You can either pass a supported string directly, or use the
ValueStateenum:TypeScriptimport ValueState from '@ui5/webcomponents-base/dist/types/ValueState.js';TypeScript<ListItemStandard additionalText="finished" additionalTextState={ValueState.Positive} > Activity 1 </ListItemStandard> <ListItemStandard additionalText="failed" additionalTextState={ValueState.Negative} > Activity 2 </ListItemStandard>The “Progress” card now shows two list items, but both of them are already completed. Let’s create two more activities which are still in progress.
First, create two
ListItemCustoms below the completed items.TypeScript<ListItemCustom></ListItemCustom> <ListItemCustom></ListItemCustom>The
ListItemCustomallows customizing the content of the list item and for this reason doesn’t offer props likeadditionalText.To show the progress, add the
ProgressIndicatoras child of the items, with the following props:value: The value, which indicates the progressvalueState: The value-state (color) of the indicator
TypeScript<ListItemCustom> <ProgressIndicator value={89} valueState={ValueState.Positive} /> </ListItemCustom> <ListItemCustom> <ProgressIndicator value={5} valueState={ValueState.Negative} /> </ListItemCustom>The indicators are displayed as part of the list item, but the title and status of the activities is still missing. For this, add two
Textcomponents above theProgressIndicatorof the firstListItemCustom:TypeScript<ListItemCustom> <Text>Activity 3</Text> <Text>in progress</Text> <ProgressIndicator value={89} valueState={ValueState.Positive} /> </ListItemCustom>All necessary information are now available, but the formatting doesn’t look right. Let’s fix that by using a flex-box:
TypeScript<ListItemCustom> <FlexBox direction={FlexBoxDirection.Column} fitContainer> <FlexBox justifyContent={FlexBoxJustifyContent.SpaceBetween}> <Text>Activity 3</Text> <Text>in progress</Text> </FlexBox> <ProgressIndicator value={89} valueState={ValueState.Positive} /> </FlexBox> </ListItemCustom>The
FlexBoxcomponent implements most of theCSS Flexboxbehavior without being forced to actually use CSS or other styling methods.The content of the list item is now aligned correctly, but doesn’t apply the correct padding, colors and dimensions. To fix this, pass the
styleprop using the default ReactinlineStylesyntax. Again, we’re using global CSS variables for this:TypeScript<ListItemCustom> <FlexBox direction={FlexBoxDirection.Column} fitContainer style={{ paddingBlock: "var(--sapContent_Space_S)" }} > <FlexBox justifyContent={FlexBoxJustifyContent.SpaceBetween}> <Text style={{ fontSize: "var(--sapFontLargeSize)" }}> Activity 3 </Text> <Text style={{ color: "var(--sapCriticalTextColor)" }}> in progress </Text> </FlexBox> <ProgressIndicator value={89} valueState={ValueState.Positive} style={{ marginBlockStart: "0.5rem" }} /> </FlexBox> </ListItemCustom>Finally, apply the same layout and styles to the content of the second
ListItemCustom.
List
The analytical dashboard should also contain a
AnalyticalTablecomponent. Again, create aCard(to wrap the Table). This time set themax-widthto900px.TypeScript<Card header={ <CardHeader titleText="AnalyticalTable" avatar={<Icon name={tableViewIcon} />} /> } style={{ maxWidth: "900px" }} > <AnalyticalTable /> </Card>Also import the
table-viewIcon.TypeScriptimport tableViewIcon from "@ui5/webcomponents-icons/dist/table-view.js";At this point, the
AnalyticalTablethrows an error due to the missing requireddataprop. You will fix this in the next.Add data and columns to the table. The
columnsprop expects an array of objects that include at least theaccessorto the data or a uniqueidproperty. The value ofHeaderwill be shown as column header.You can create your own data or just use the code below and paste it right after the definition of the
datasetof the chart.TypeScriptconst tableData = new Array(500).fill(null).map((_, index) => { return { name: `name${index}`, age: Math.floor(Math.random() * 100), friend: { name: `friend.Name${index}`, age: Math.floor(Math.random() * 100) } }; }); const tableColumns = [ { Header: "Name", accessor: "name" // String-based value accessors! }, { Header: "Age", accessor: "age" }, { Header: "Friend Name", accessor: "friend.name" }, { Header: "Friend Age", accessor: "friend.age" } ];Display the data by replacing the current table with.
TypeScript<AnalyticalTable data={tableData} columns={tableColumns} />
Table Add more properties:
You can add many more properties to the
AnalyticalTablecomponent. For example, you can allow sorting, filtering and grouping viasortable,filterableandgroupable, enable different selection modes withselectionMode, control how the table splits up available space between the columns withscaleWidthModeand much more. Feel free to take a look at the documentation and explore the different examples before continuing.The default visible rows count is at 15. This number is a bit to high for a dashboard table. Reduce the
visibleRowscount to 5 by setting the corresponding prop.TypeScript<AnalyticalTable data={tableData} columns={tableColumns} visibleRows={5}/>
At the moment, the dashboard doesn’t really look like a dashboard. The components are too close to each other and not aligned correctly. Let’s change that.
Add margin to each of the three
Cards:To add a margin to the cards, you can use the global CSS vars again:
TypeScript<Card header={ <CardHeader titleText="Stock Prices" ... /> } style={{ width: "300px", margin: "var(--sapContent_Margin_Small)" }} >TypeScript<Card header={ <CardHeader titleText="Progress" ... /> } style={{ width: "300px", margin: "var(--sapContent_Margin_Small)" }} >TypeScript<Card header={ <CardHeader titleText="AnalyticalTable" ... /> } style={{ maxWidth: "900px", margin: "var(--sapContent_Margin_Small)" }} >Align the elements:
To properly align the
Cards, use aFlexBoxcomponent and wrap yourCardsinside of it. Use thejustifyContentprop to center align all elements andwrapto make them move to the next line if not enough space is available. Also apply thestyleprop to add a padding to the whole content area.TypeScript<FlexBox justifyContent={FlexBoxJustifyContent.Center} wrap={FlexBoxWrap.Wrap} style={{ padding: "var(--sapContent_Space_M)" }} ... </FlexBox>Note: You could also use the
gapprop of theFlexBoxcomponent to apply the spacing.
Your component should now look like this:

import tableViewIcon from "@ui5/webcomponents-icons/dist/table-view.js";
import listIcon from "@ui5/webcomponents-icons/dist/list.js";
import activateIcon from "@ui5/webcomponents-icons/dist/activate.js";
import lineChartIcon from "@ui5/webcomponents-icons/dist/line-chart.js";
import barChartIcon from "@ui5/webcomponents-icons/dist/horizontal-bar-chart.js";
import { useState } from "react";
import {
Avatar,
Card,
CardHeader,
Text,
ShellBar,
ShellBarItem,
List,
ListItemStandard,
ListItemCustom,
ProgressIndicator,
FlexBox,
FlexBoxJustifyContent,
FlexBoxWrap,
FlexBoxDirection,
AnalyticalTable,
Icon,
} from "@ui5/webcomponents-react";
import { BarChart, LineChart } from "@ui5/webcomponents-react-charts";
import reactLogo from "./assets/reactLogo.png";
import profilePictureExample from "./assets/profilePictureExample.png";
import ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
const tableData = new Array(500).fill(null).map((_, index) => {
return {
name: `name${index}`,
age: Math.floor(Math.random() * 100),
friend: {
name: `friend.Name${index}`,
age: Math.floor(Math.random() * 100),
},
};
});
const tableColumns = [
{
Header: "Name",
accessor: "name", // String-based value accessors!
},
{
Header: "Age",
accessor: "age",
},
{
Header: "Friend Name",
accessor: "friend.name",
},
{
Header: "Friend Age",
accessor: "friend.age",
},
];
const dataset = [
{
month: "January",
data: 65,
},
{
month: "February",
data: 59,
},
{
month: "March",
data: 80,
},
{
month: "April",
data: 81,
},
{
month: "May",
data: 56,
},
{
month: "June",
data: 55,
},
{
month: "July",
data: 40,
},
];
export function MyApp() {
const [toggleCharts, setToggleCharts] = useState("lineChart");
const [loading, setLoading] = useState(false);
const contentTitle =
toggleCharts === "lineChart" ? "Line Chart" : "Bar Chart";
const switchToChart =
toggleCharts === "lineChart" ? "Bar Chart" : "Line Chart";
const handleHeaderClick = () => {
if (toggleCharts === "lineChart") {
setLoading(true);
setTimeout(() => {
setLoading(false);
setToggleCharts("barChart");
}, 2000);
} else {
setLoading(true);
setTimeout(() => {
setLoading(false);
setToggleCharts("lineChart");
}, 2000);
}
};
return (
<div>
<ShellBar
logo={<img src={reactLogo} alt="Company Logo" />}
profile={
<Avatar>
<img src={profilePictureExample} alt="User Avatar" />
</Avatar>
}
primaryTitle="My App"
>
<ShellBarItem icon={activateIcon} text="Activate" />
</ShellBar>
<FlexBox
justifyContent={FlexBoxJustifyContent.Center}
wrap={FlexBoxWrap.Wrap}
style={{ padding: "var(--sapContent_Space_M)" }}
>
<Card
header={
<CardHeader
titleText="Prices"
subtitleText={`Click here to switch to ${switchToChart}`}
interactive
avatar={
<Icon
name={
toggleCharts === "lineChart" ? lineChartIcon : barChartIcon
}
accessibleName={contentTitle}
/>
}
onClick={handleHeaderClick}
/>
}
style={{ width: "300px", margin: "var(--sapContent_Margin_Small)" }}
>
<Text style={{ padding: "var(--sapContent_Space_S)" }}>
{contentTitle}
</Text>
{toggleCharts === "lineChart" ? (
<LineChart
dimensions={[{ accessor: "month" }]}
measures={[{ accessor: "data", label: "Price" }]}
dataset={dataset}
loading={loading}
/>
) : (
<BarChart
dimensions={[{ accessor: "month" }]}
measures={[{ accessor: "data", label: "Price" }]}
dataset={dataset}
loading={loading}
/>
)}
</Card>
<Card
header={
<CardHeader
titleText="Progress"
subtitleText="List"
avatar={<Icon name={listIcon} />}
/>
}
style={{ width: "300px", margin: "var(--sapContent_Margin_Small)" }}
>
<List>
<ListItemStandard
additionalText="finished"
additionalTextState={ValueState.Positive}
>
Activity 1
</ListItemStandard>
<ListItemStandard
additionalText="failed"
additionalTextState={ValueState.Negative}
>
Activity 2
</ListItemStandard>
<ListItemCustom>
<FlexBox
direction={FlexBoxDirection.Column}
fitContainer
style={{ paddingBlock: "var(--sapContent_Space_S)" }}
>
<FlexBox justifyContent={FlexBoxJustifyContent.SpaceBetween}>
<Text style={{ fontSize: "var(--sapFontLargeSize)" }}>
Activity 3
</Text>
<Text style={{ color: "var(--sapCriticalTextColor)" }}>
in progress
</Text>
</FlexBox>
<ProgressIndicator
value={89}
valueState={ValueState.Positive}
style={{ marginBlockStart: "0.5rem" }}
/>
</FlexBox>
</ListItemCustom>
<ListItemCustom>
<FlexBox
direction={FlexBoxDirection.Column}
fitContainer
style={{ paddingBlock: "var(--sapContent_Space_S)" }}
>
<FlexBox justifyContent={FlexBoxJustifyContent.SpaceBetween}>
<Text style={{ fontSize: "var(--sapFontLargeSize)" }}>
Activity 3
</Text>
<Text style={{ color: "var(--sapCriticalTextColor)" }}>
in progress
</Text>
</FlexBox>
<ProgressIndicator
value={5}
valueState={ValueState.Negative}
style={{ marginBlockStart: "0.5rem" }}
/>
</FlexBox>
</ListItemCustom>
</List>
</Card>
<Card
header={
<CardHeader
titleText="AnalyticalTable"
avatar={<Icon name={tableViewIcon} />}
/>
}
style={{
maxWidth: "900px",
margin: "var(--sapContent_Margin_Small)",
}}
>
<AnalyticalTable
data={tableData}
columns={tableColumns}
visibleRows={5}
/>
</Card>
</FlexBox>
</div>
);
}Resources
Discussion
Share feedback on this tutorial or join the conversation in SAP Community.