Curves Mobile App Kit

Components

Every component in the kit, with its markup. Each one has a live demo page in components/; copy from there and change the words.

Buttons

Use one main button per screen. Demo.

<button class="mk-btn" type="button">Send money</button>
<button class="mk-btn mk-btn--secondary" type="button">Save as draft</button>
<button class="mk-btn mk-btn--icon mk-btn--round" type="button" aria-label="Share">…icon…</button>
<button class="mk-btn" type="button" aria-busy="true"><span>Sending</span></button>

Form fields

Demo. A visible label for every field; the control box holds icons, affixes and buttons.

<div class="mk-field">
  <label class="mk-field__label" for="amount">Amount</label>
  <div class="mk-field__control">
    <span class="mk-field__affix">SAR</span>
    <input class="mk-input" id="amount" inputmode="decimal" aria-describedby="amount-hint">
  </div>
  <p class="mk-field__hint" id="amount-hint">Up to SAR 20,000 a day</p>
</div>

Choices and switches

<label class="mk-check"><input type="checkbox"> <span>Remember me</span></label>
<label class="mk-check"><input type="radio" name="speed"> <span>Instant</span></label>
<label class="mk-switch"><span>Notifications</span><input type="checkbox" role="switch"></label>

Native inputs keep keyboard and screen-reader support. The switch knob moves the other way in Arabic.

Lists

Demo. For settings, activity and contacts.

<ul class="mk-list mk-list--card mk-list--divided">
  <li><a class="mk-list__item" href="detail.html">
    <span class="mk-list__media">…icon…</span>
    <span class="mk-list__body">
      <span class="mk-list__title">Corner Café</span>
      <span class="mk-list__subtitle">Today, 9:24</span>
    </span>
    <span class="mk-list__end"><span class="mk-amount">−SAR 18.50</span></span>
  </a></li>
</ul>

Rows can be links, buttons or plain <div class="mk-list__item">. Group headings use .mk-list__heading.

Cards and covers

Demo.

<div class="mk-card mk-card--curve">…</div>

<div class="mk-cover" style="--mk-cover-height: 13rem">
  <img class="mk-cover__image" src="photo.jpg" alt="">
  <div class="mk-cover__scrim"></div>
  <div class="mk-cover__bottom"><strong>Mountains in winter</strong></div>
</div>

Chips, badges, avatars

<div class="mk-scroller" data-mk-single>
  <button class="mk-chip" type="button" aria-pressed="true">All</button>
  <button class="mk-chip" type="button" aria-pressed="false">Dining</button>
</div>
<span class="mk-badge mk-badge--success">Paid</span>
<span class="mk-avatar mk-avatar--lg"><img src="face.jpg" alt="Emma Johnson"></span>

Chips toggle on tap; inside data-mk-single only one stays pressed, and the container gets an mk:chip event. Badge tones: default (danger), --accent, --success, --warning, --neutral, --dot.

Alerts

<div class="mk-alert mk-alert--warning" role="alert">
  …icon…
  <div class="mk-alert__body"><strong class="mk-alert__title">Bill due tomorrow</strong><p>Pay now to avoid a late fee.</p></div>
</div>

Tones: default (info), --success, --warning, --danger. Use role="alert" for warnings and errors, role="status" for the rest.

Toasts and snackbars

<button type="button" data-mk-toast="Copied" data-mk-toast-tone="success">Copy</button>
<script type="module">
  MK.toast("Transaction deleted", {
    position: "bottom",
    action: { label: "Undo", onClick: () => restore() },
  });
</script>

Toasts are announced to screen readers through a polite live region. Options: tone (info, success, warning, danger), position (top or bottom), duration in ms (0 keeps it until closed).

Progress

<div class="mk-progress" role="progressbar" aria-label="Dining budget"
     aria-valuenow="64" aria-valuemin="0" aria-valuemax="100" style="--mk-value: 64"></div>
<div class="mk-ring" style="--mk-value: 72" role="img" aria-label="72% saved"><span>72%</span></div>
<span class="mk-spinner" role="status" aria-label="Loading"></span>

Skeleton and empty states

<span class="mk-skeleton" style="inline-size: 60%" aria-hidden="true"></span>

<div class="mk-empty">
  <div class="mk-empty__icon">…icon…</div>
  <h2 class="mk-empty__title">No bills yet</h2>
  <p>Add your first bill and we'll remind you before it's due.</p>
  <button class="mk-btn" type="button">Add a bill</button>
</div>

Skeletons stop shimmering when the user prefers reduced motion.

Tabs and segments

Demo. Tabs follow the ARIA tabs pattern; arrow keys move between tabs, mirrored in Arabic.

<div class="mk-tabs mk-tabs--pills" role="tablist" aria-label="Activity type">
  <button class="mk-tabs__tab" role="tab" id="t1" aria-controls="p1" aria-selected="true">All</button>
  <button class="mk-tabs__tab" role="tab" id="t2" aria-controls="p2" aria-selected="false">Money in</button>
</div>
<div class="mk-tabs__panel" role="tabpanel" id="p1" aria-labelledby="t1">…</div>
<div class="mk-tabs__panel" role="tabpanel" id="p2" aria-labelledby="t2" hidden>…</div>

<fieldset class="mk-segmented">
  <legend class="mk-visually-hidden">Period</legend>
  <label><input type="radio" name="p" checked><span>Month</span></label>
  <label><input type="radio" name="p"><span>Year</span></label>
</fieldset>

Tab styles: underline (default), --fill, --pills. The segmented control is radios and needs no script.

Accordion

<details class="mk-accordion"><summary>How long does a transfer take?</summary>
  <div class="mk-accordion__body">Local transfers are instant.</div></details>

Steps and timeline

<ol class="mk-steps" aria-label="Transfer progress">
  <li class="is-done">Recipient</li><li aria-current="step">Review</li><li>Done</li>
</ol>
<ol class="mk-timeline">
  <li><span class="mk-timeline__time">9:24</span><strong class="mk-timeline__title">Card ordered</strong></li>
</ol>

Demo. Splide 4.1.4, loaded only on pages that have a carousel. Swipe, keyboard and Arabic direction work out of the box; options go in the attribute as JSON (see the Splide options).

<section class="mk-carousel splide" aria-label="Offers" data-mk-carousel='{"perPage":1,"padding":{"right":"2.5rem"}}'>
  <div class="splide__track"><div class="splide__list">
    <div class="splide__slide">…</div>
  </div></div>
</section>

mk-carousel--cover sets the dots over full-screen slides, as in the onboarding page. The Splide instance is on element.mkSplide.

<form class="mk-search" role="search">
  …icon…
  <input type="search" aria-label="Search contacts" data-mk-filter="#contacts" data-mk-filter-empty="#none">
</form>
<ul id="contacts"><li data-mk-keywords="sarah miller">…</li></ul>
<p id="none" hidden>No matches</p>

The filter hides children of the target that don't match, using data-mk-keywords or their text. For server search, drop data-mk-filter and handle the input yourself.

File upload

<label class="mk-upload">
  <input type="file" accept="image/*" multiple data-mk-files="files">
  …icon… <strong>Add a receipt</strong> <span>Photo or PDF, up to 5 MB</span>
</label>
<ul class="mk-upload__files" id="files" aria-live="polite"></ul>

Chart

Chart.js 4.6.1 with the kit's colours, fonts, dark mode and Arabic labels. Charts redraw when the theme or language changes.

<div class="mk-chart"><canvas id="spend" role="img" aria-label="Spending by month: …"></canvas></div>
<script src="dist/vendor/chart.umd.min.js"></script>
<script src="dist/js/mk-chart.js"></script>
<script>
  MKChart.bar(document.getElementById("spend"), {
    labels: ["Jun", "Jul", "Aug"], labelsAr: ["يونيو", "يوليو", "أغسطس"], values: [3200, 4100, 2800],
  });
</script>

Also MKChart.line and MKChart.doughnut. Give each canvas an aria-label that states the numbers.

<div class="mk-gallery">
  <a class="mk-lightbox" href="big.jpg" data-gallery="trip" aria-label="Beach"><img src="thumb.jpg" alt=""></a>
</div>

GLightbox 3.3.1 opens full screen with swipe, loaded only where a gallery exists. Tooltips: class="mk-tip" data-mk-tip="Copy". Ratings: <span class="mk-rating" style="--mk-value: 4.5" role="img" aria-label="4.5 out of 5">.

A short list of actions from a "more" button. It uses the browser's own popover, so a tap outside or Escape closes it. Demo.

<button class="mk-btn mk-btn--icon mk-btn--ghost" type="button" popovertarget="card-menu" aria-label="More options">…icon…</button>
<div class="mk-menu" id="card-menu" popover>
  <button class="mk-menu__item" type="button">…icon…<span>Rename card</span></button>
  <hr class="mk-menu__divider">
  <button class="mk-menu__item mk-menu__item--danger" type="button">…icon…<span>Freeze card</span></button>
</div>

Pagination and load more

<nav aria-label="Pages"><ul class="mk-pagination" data-mk-pages="12">
  <li><a href="#" data-mk-page="prev" aria-label="Previous page" aria-disabled="true">…icon…</a></li>
  <li><a href="#" data-mk-page="1" aria-current="page">1</a></li>
  <li hidden><span class="mk-pagination__gap" aria-hidden="true" data-mk-gap="start">…</span></li>
  <li><a href="#" data-mk-page="2">2</a></li>
  <li><a href="#" data-mk-page="3">3</a></li>
  <li><span class="mk-pagination__gap" aria-hidden="true" data-mk-gap="end">…</span></li>
  <li><a href="#" data-mk-page="12">12</a></li>
  <li><a href="#" data-mk-page="next" aria-label="Next page">…icon…</a></li>
</ul></nav>

<ul class="mk-list" id="feed-list">…first items…</ul>
<template id="feed-more">…more items…</template>
<button class="mk-btn mk-btn--secondary mk-btn--block" type="button" data-mk-more="feed-list" data-mk-more-from="feed-more" data-mk-more-step="4" data-mk-more-auto>Load more</button>
<p class="mk-caption mk-more-status" data-mk-more-status="feed-list" aria-live="polite"></p>

Pull to refresh, swipe actions, reorder

Each gesture has a button or key that does the same thing. Demo.

<div class="mk-pull" data-mk-pull id="inbox">
  <div class="mk-pull__indicator" aria-hidden="true"><span>…icon…</span></div>
  <div class="mk-pull__content">…list…</div>
</div>
<button class="mk-btn mk-btn--sm mk-btn--ghost" type="button" data-mk-refresh="inbox">Refresh</button>

<li class="mk-swipe">
  <div class="mk-swipe__actions">
    <button class="mk-swipe__action mk-swipe__action--archive" type="button" data-mk-swipe-remove="Archived">…icon…<span>Archive</span></button>
    <button class="mk-swipe__action mk-swipe__action--danger" type="button" data-mk-swipe-remove="Deleted">…icon…<span>Delete</span></button>
  </div>
  <div class="mk-swipe__content"><a class="mk-list__item" href="…" draggable="false">…row…</a></div>
</li>

<ul class="mk-list mk-sortable" data-mk-reorder aria-label="Home screen order">
  <li><div class="mk-list__item">…<span class="mk-list__title">Balance</span><button class="mk-drag" type="button" aria-label="Move Balance (arrow keys)">…icon…</button></div></li>
</ul>

Back to top, hiding bars, reading progress

<body data-mk-autohide>
<button class="mk-to-top" type="button" data-mk-to-top hidden aria-label="Back to top">…icon…</button>
<div class="mk-reading" aria-hidden="true"></div>

Date, time and search with suggestions

<input class="mk-input" id="f-date" type="date">
<input class="mk-input" id="f-time" type="time" step="900">

<div class="mk-field mk-combo">
  <label class="mk-field__label" for="f-city">Destination</label>
  <div class="mk-field__control">…icon…<input class="mk-input" id="f-city" role="combobox" aria-expanded="false" aria-controls="f-city-list" aria-autocomplete="list" autocomplete="off"></div>
  <ul class="mk-combo__list" id="f-city-list" role="listbox" aria-label="Cities" hidden>
    <li class="mk-combo__option" role="option" id="c-ruh" data-mk-keywords="riyadh الرياض">…icon…<span class="mk-combo__text">Riyadh</span></li>
    <li class="mk-combo__option mk-combo__none" role="option" aria-disabled="true" hidden>No matches</li>
  </ul>
</div>

Speed dial

<div class="mk-speed-dial">
  <button class="mk-fab" type="button" aria-expanded="false" aria-controls="dial-actions" aria-label="New">…plus icon…</button>
  <ul class="mk-speed-dial__actions" id="dial-actions" hidden>
    <li><button class="mk-speed-dial__action" type="button"><span>Transfer</span>…icon…</button></li>
  </ul>
</div>

The plus turns into a close mark, the page dims, and Escape or a tap outside closes it. Keep it to three or four actions.

Expandable card and opening hours

<details class="mk-card mk-expand">
  <summary>…title…<span class="mk-expand__chevron">…chevron icon…</span></summary>
  <div class="mk-expand__body">…details…</div>
</details>

<div class="mk-card" data-mk-hours data-mk-tz="Asia/Riyadh">
  <span class="mk-hours__status"></span>
  <ul class="mk-hours__week">
    <li data-day="4" data-open="07:00" data-close="01:00"><span>Thursday</span><span class="mk-ltr">07:00 – 01:00</span></li>
    <li data-day="5" data-closed><span>Friday</span><span>Closed</span></li>
  </ul>
</div>

Before and after

<div class="mk-compare" style="aspect-ratio:3/2">
  <img src="before.jpg" alt="Before: …">
  <img src="after.jpg" alt="After: …">
  <span class="mk-compare__label">Before</span>
  <span class="mk-compare__label mk-compare__label--after">After</span>
  <span class="mk-compare__line" aria-hidden="true"></span>
  <input class="mk-compare__range" type="range" min="0" max="100" value="50" aria-label="Before and after">
</div>

A real range input covers the photo, so dragging anywhere and the arrow keys both move the line.

<div class="mk-consent" data-mk-consent role="region" aria-labelledby="consent-title" hidden>
  <h2 class="mk-consent__title" id="consent-title">…icon…<span>Cookies</span></h2>
  <p>We use the cookies the app needs to work, and optional ones to improve it. Your choice.</p>
  <div class="mk-consent__actions">
    <button class="mk-btn mk-btn--secondary" type="button" data-mk-consent-choice="needed">Only needed</button>
    <button class="mk-btn mk-btn--secondary" type="button" data-mk-consent-choice="all">Accept all</button>
  </div>
</div>

Share, location, vibration, text size, page transitions

Demo.

<button class="mk-btn" type="button" data-mk-share data-mk-share-title="Curves Mobile App Kit">Share</button>
<button class="mk-btn mk-btn--secondary" type="button" data-mk-locate="where">Use my location</button>
<output id="where" aria-live="polite"></output>
<button class="mk-btn" type="button" data-mk-haptic>Pay</button>
<button class="mk-chip" type="button" data-mk-set="text:lg" aria-label="Large">A</button>

App patterns

ClassWhere it's used
mk-bankcard (--dark, --light, is-frozen)Wallet cards
mk-quick, mk-quick__item, mk-quick__iconQuick actions on home screens
mk-amount-input, mk-keypadEntering an amount
mk-qrA code to scan (the demo code is a placeholder)
mk-ticket, mk-routeBoarding pass and routes
mk-success, mk-rowsSuccess screen and receipts
mk-auth, mk-splash, mk-walk, mk-chat, mk-bubble, mk-composer, mk-profile, mk-stats, mk-calendar, mk-plan, mk-statePage templates