Products · Build a dot

Components

Pick the right Dot component for each part of a page, from layout, text and lists to forms, amounts, feedback and sheets.

Components look and behave like the rest of Dash. Each name below links to its page in the Components reference, which lists every prop and event.

Every prop is optional. If you use a prop that a component doesn't have, the build tells you. Events work as described in Views.

Lay out a page

ComponentUse it for
ViewColumns, rows and cards. See Layout and styling
StackText over a picture, and other layers. See Layers with Stack
Grid, GridItemA home screen of services, in equal columns
ScrollA row that scrolls sideways
CarouselSlides, one at a time
FooterThe page's main button, kept at the bottom while the page scrolls. Put it last
PinnedAnything else kept on the top or bottom edge, such as the page's own tabs
DividerA line between sections

Show text, amounts and pictures

ComponentUse it for
TextAll text. variant sets its role, from display for a big amount down to caption
LinkText to tap. An https href opens in the phone's browser
ImageA picture from statics/, or from a host in network.hosts
AvatarA photo in a circle, or initials until it loads
IconOne of Dash's icons
BadgeA short status, like Paid
QRCodeA ticket or other code for someone else to scan
XML
<View variant="card" gap="small">
  <View direction="row" gap="medium" align="center">
    <Avatar name="Mariama Kamara" image="{{ photo }}" />
    <Text variant="subtitle">Mariama Kamara</Text>
  </View>
  <Text variant="caption" tone="muted">Balance</Text>
  <Text variant="display" numeric>SLE {{ balance }}</Text>
</View>

Set numeric on amounts so the digits line up. Give an Image an aspect, and its height follows from its width.

Lists and details

A List holds ListItem rows in a card. For a receipt, use a DetailRow for each label and its value. A Disclosure is a row that opens to show more, like an FAQ answer. Steps with Step show what has happened and what comes next.

This list of payments loads more as the person scrolls down:

XML
<List onEndReached="loadMore">
  <For each="{{ payments }}" item="payment" key="id">
    <ListItem
      icon="receipt"
      title="{{ payment.name }}"
      value="SLE {{ payment.amount }}"
      data-id="{{ payment.id }}"
      onTap="open"
    />
  </For>
</List>

loadMore returns early while a batch is loading, and after the last one:

TypeScript
async loadMore(state) {
  if (state.loading || state.allLoaded) {
    return;
  }
  this.setState({ loading: true, error: '' });
  try {
    const url = `${env.API_URL}/payments?skip=${state.payments.length}`;
    const response = await this.dash.network.http.request<Payment[]>({ url, as: 'json' });
    if (!response.ok) {
      throw new Error();
    }
    this.setState({
      payments: [...this.state.payments, ...response.body],
      allLoaded: response.body.length === 0,
    });
  } catch {
    this.setState({ error: "We couldn't load more payments." });
  } finally {
    this.setState({ loading: false });
  }
},

Long text to read

For news and guides, wrap your text in Prose for comfortable spacing. It works with Quote and BulletList. For Markdown from your server, use RichText, which is safe: HTML shows as plain text, and only https links open.

Forms

ComponentUse it for
InputOne line of text. type picks the keyboard
TextAreaSeveral lines of text
Select, OptionOne choice from a long list
RadioGroup, RadioOne choice from a few
Switch, CheckboxOn or off
Stepper, SliderA number
ButtonAn action. variant is primary, secondary, outline or ghost, tone colors it, like danger for one that deletes, and size makes it smaller or larger. loading shows a spinner and ignores taps

A field shows what's in state. Pass the value in, and update it from the field's change event. This one sends airtime to a cousin in Kenema:

XML
<Input
  label="Phone number"
  type="phone"
  prefix="+232"
  value="{{ phone }}"
  error="{{ phoneError }}"
  onChange="setPhone"
/>
<Select label="Network" value="{{ network }}" onChange="setNetwork">
  <Option value="orange" label="Orange" />
  <Option value="africell" label="Africell" />
  <Option value="qcell" label="Qcell" />
</Select>
<Switch label="Save this number" checked="{{ save }}" onChange="setSave" />
<Button onClick="send" loading="{{ sending }}">Send airtime</Button>
TypeScript
mutations: {
  setPhone: (_state, event: ValueEvent) => ({ phone: event.detail.value, phoneError: '' }),
  setNetwork: (_state, event: ValueEvent) => ({ network: event.detail.value }),
  setSave: (_state, event: CheckedEvent) => ({ save: event.detail.checked }),
},
actions: {
  async send(state) {
    if (state.phone.replace(/\D/g, '').length !== 8) {
      this.setState({ phoneError: 'Enter 8 digits, such as 76 123456.' });
      return;
    }
    // Send the airtime.
  },
},

Set error to show a problem under a field, as send does. For a search box, use type="search" and see Search as the person types.

Amounts and codes

AmountInput takes a sum of money. CodeInput takes a one-time code or PIN, and picks up a code the phone reads from an SMS.

XML
<AmountInput label="Amount" value="{{ amount }}" onChange="setAmount" />
<CodeInput
  label="Enter the code we sent you"
  value="{{ code }}"
  onChange="setCode"
  onComplete="verify"
/>
  • AmountInput counts in the smallest unit, like the Monime API, so 150 is SLE 1.50. Set unit="major" to count whole amounts, so 150 is SLE 150. It sends null when empty, so type its event as ValueEvent<number | null>.
  • CodeInput sends complete once the last digit is in, so check the code there.

Tabs and filters

Tabs switch between views of one page. Chips narrow a list. Tabs only records which tab is chosen, so show each tab's content with an <If>:

XML
<Tabs value="{{ tab }}" variant="segmented" onChange="setTab">
  <Tab value="sent" label="Sent" />
  <Tab value="received" label="Received" badge="{{ unread }}" />
</Tabs>
<If condition="{{ tab === 'sent' }}">
  <Text>Money you sent</Text>
</If>
<Else>
  <Text>Money you received</Text>
</Else>

A Tab can also have an icon, which shows before its label. A disabled Tab can't be chosen.

A page's own navigation

For tabs that stay on screen, put Tabs with variant="bar" in a Pinned. The tabs share the width equally, with each icon over its label and each badge on its icon's corner:

XML
<View>
  <If condition="{{ tab === 'home' }}">
    <Text>Today's specials</Text>
  </If>
  <ElseIf condition="{{ tab === 'orders' }}">
    <Text>Your orders</Text>
  </ElseIf>
  <Else>
    <Text>Your account</Text>
  </Else>

  <Pinned edge="bottom" border="subtle">
    <Tabs variant="bar" value="{{ tab }}" onChange="setTab">
      <Tab value="home" label="Home" icon="home" />
      <Tab value="orders" label="Orders" icon="receipt" badge="{{ newOrders }}" />
      <Tab value="account" label="Account" icon="user" />
    </Tabs>
  </Pinned>
</View>
Three tabs along the bottom of the screen, each with an icon over its label

Set tabBar: { hidden: true } in the page's configs, so Dash's tab bar doesn't show under yours.

Feedback

ComponentUse it for
SpinnerSomething's loading
SkeletonA placeholder shaped like what's loading
ProgressBarHow far along an upload is, from 0 to 100
NoticeA message that stays on the page
EmptyStateWhat to show when there's nothing yet
XML
<EmptyState icon="receipt" title="No payments yet" text="Payments you make show here.">
  <Button onClick="pay">Make a payment</Button>
</EmptyState>

Sheets and toasts

A Sheet slides up over the page. A Toast shows a quick message, like "Copied". Both follow the state:

  • A Sheet shows while open is true. Tapping outside it sends close, and your handler sets open back to false.
  • A Toast shows its message, then sends hide. Clear the message there so it can show again.
XML
<Sheet open="{{ selected !== null }}" title="Payment" onClose="closeSheet">
  <Button variant="secondary" onClick="copyReference">Copy reference</Button>
  <Button onClick="closeSheet">Cancel</Button>
</Sheet>
<Toast message="{{ toast }}" tone="success" onHide="clearToast" />

Keep reading