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
- Form fields
- Choices and switches
- Lists
- Cards and covers
- Chips, badges, avatars
- Alerts
- Toasts and snackbars
- Progress
- Skeleton and empty states
- Tabs and segments
- Accordion
- Steps and timeline
- Carousel
- Search
- File upload
- Chart
- Gallery
- Dropdown menu
- Pagination and load more
- Pull to refresh, swipe, reorder
- Back to top, hiding bars, reading progress
- Date, time and suggestions
- Speed dial
- Expandable card and opening hours
- Before and after
- Cookie notice
- Share, location, vibration, text size
- App patterns
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>- Styles:
--secondary,--ghost,--danger,--curve(the signature corner). - Sizes:
--sm,--lg,--block(full width). Group with.mk-btn-group. - Floating action button:
<button class="mk-fab" aria-label="…">, fixed above the bottom bar. - Icon-only buttons always need an
aria-label. Loading buttons keep their label for screen readers.
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>- Errors: add
is-invalidto.mk-field,aria-invalid="true"to the input, and a.mk-field__errorlinked witharia-describedby. - Also:
.mk-select,.mk-textarea, show-password button (data-mk-reveal="input-id"), one-time code (.mk-otp), range (.mk-range), counter (.mk-stepper). - Inputs use 16px text, so iOS doesn't zoom in when a field is tapped.
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>- Card styles:
--curve,--outlined,--sunken,--accent(bar colour),--flush(no padding, for images). - Cover slots:
__top,__center,__bottom. Without an image the cover shows the theme gradient.mk-cover--fullfills the screen. - The scrim keeps white text readable on any photo.
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>Carousel
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.
Search
<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.
Gallery and photo viewer
<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">.
Dropdown menu
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>- It opens under its button, aligned to the end, and above it when there's no room below. Arrow keys, Home and End move between items.
- A choice closes the menu. Add
data-mk-keepto an item that should leave it open.
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>- Pagination moves
aria-current, keeps the first and last page, and firesmk:pagewithdetail.page. Plain links (href="?page=2", nodata-mk-pages) work too. - Load more takes the next items from the
<template>. Withdata-mk-more-autoit loads by itself as the button comes into view (an endless list), and the button stays for anyone who prefers it. - Real data: listen for
mk:moreon the list, callevent.preventDefault(), add your items, then callevent.detail.done(hasMore).
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>- Pull to refresh works with touch and mouse at the top of the page. Listen for
mk:refresh; callpreventDefault()andevent.detail.done()when your data is in, or the kit finishes after a moment. - Swipe a row toward the start to show its actions (mirrored in Arabic). The Tab key also reaches the actions and opens the row.
data-mk-swipe-removeremoves the row with an Undo toast; leave it out and handle the click yourself. - Reorder by dragging the handle, or focus it and press the up and down arrows. The new position is announced, and
mk:reorderfires.
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>data-mk-autohideslides both curved bars away while you scroll down and brings them back when you scroll up. In the page generator, set"autohide": truein the page header.- Back to top appears after 600px and moves focus to the page heading. It sits above a floating action button when there is one.
- Reading progress fills a thin line along the top. Pure CSS where scroll-driven animations exist, a few lines of script elsewhere.
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>- Date and time fields use the phone's own pickers, inside the usual field frame.
- Suggestions filter as you type, highlight the match, and open above the field when the bottom bar leaves no room. Arrows, Enter and Escape work.
data-mk-keywordslets English and Arabic spellings both match. The choice firesmk:selectwithdetail.value.
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>- The expandable card is a native
<details>: it works without script, and opens smoothly where the browser can animate it. - Opening hours shows "Open now", "Closes soon" (the last hour) or "Closed · opens …" in the shop's time zone, highlights today and updates every minute. Day 0 is Sunday; a close time before the open time runs past midnight.
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.
Cookie notice
<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>- It shows until a choice is saved.
MK.consent()returns"all","needed"ornull;mk:consentfires when someone chooses. Load optional scripts only after"all". - Both buttons carry the same weight, as consent rules expect.
data-mk-consent-openon a settings link shows it again.data-mk-consent="manual"shows it only when opened.
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>- Share opens the phone's own share screen; where there is none, the link is copied.
- Location is asked for only on tap and stays on the device.
mk:locationfires with the coordinates; errors are explained in plain words. data-mk-haptic(or"double", orMK.haptic(ms)) gives a light buzz on Android; other phones ignore it.- Text size:
sm,md,lg,xlon<html data-mk-text>, saved like the theme. Everything sized in rem grows together. - Printing: bars, floating buttons, toasts and notices never print; add
.mk-print-hideto anything else.<button data-mk-print="cert">prints only the element with that id (a certificate, a receipt). - Page transitions: pages cross-fade and the bottom bar stays put (Chrome, Edge, Safari 18.2 and later), and they're off with reduced motion. Set
$mk-page-transitions: falsein Sass to turn them off.
App patterns
| Class | Where it's used |
|---|---|
mk-bankcard (--dark, --light, is-frozen) | Wallet cards |
mk-quick, mk-quick__item, mk-quick__icon | Quick actions on home screens |
mk-amount-input, mk-keypad | Entering an amount |
mk-qr | A code to scan (the demo code is a placeholder) |
mk-ticket, mk-route | Boarding pass and routes |
mk-success, mk-rows | Success screen and receipts |
mk-auth, mk-splash, mk-walk, mk-chat, mk-bubble, mk-composer, mk-profile, mk-stats, mk-calendar, mk-plan, mk-state | Page templates |