KCDS Mobile — Design system documentation
Companion to KCDS web. Native mobile shell patterns, touch & gesture rules, and product canonicals for the 3 KC products that ship mobile apps: Learner Portal, LMS Admin, and Safety. Use the platform switcher in the sidebar to jump to web.
Scope & products
KC has 17 products. Three have mobile apps in production or development. Mobile is a smaller surface than web — most products are web-only.
| Product | Mobile | Status | Tech |
|---|---|---|---|
| Learner Portal | Production v14.6 | Native iOS (Swift + SwiftUI), Android (Kotlin + Compose). ~91 white-label builds. | |
| LMS Admin | In development v1 | Native iOS + Android. Admin-first: portal config, assignments, grading. | |
| Safety | Production | React Native 0.84.1, TypeScript. Field incident reporting. | |
| Performance Management | Planned | Phase 2 | React Native (placeholder) |
| USMP | Planned | Phase 7 | Mobile SDKs |
| All other ~14 KC products | Web-only | — |
The skill's primary focus is Learner Portal mobile (~17 audited production screens, 91 tenant builds). LMS Admin inherits Learner Portal patterns with admin overrides. Safety only inherits foundations (color tokens, typography, spacing, dark theme) — shell patterns differ because it's React Native.
The six rules that override everything
Every audit, every new screen, every code review — these six checks come first. If a screen fails one of these, fix it before anything else.
- Sentence case for interactive elements. Buttons, tabs, badges, form labels, nav items, status labels.
Start coursenotStart Course. - Title Case for headings and content names. Empty state titles, modal titles, section headers, page titles, training material names.
No Courses YetnotNo courses yet. Applies to H1–H4 and any text that serves as a heading. (kc-design-foundations → capitalization.md) - Safe-area insets on every fixed/sticky element.
env(safe-area-inset-bottom)on bottom tab bar, sticky CTAs, sheet actions.env(safe-area-inset-top)on top app bar. Missing this overlaps the iOS home indicator on every modern phone. - Accent resolves from product, never hardcoded. Reference
--kcds-color-primaryor platform-specific token. Hardcoding#F97316outside Learner Portal breaks every other product. - Hit areas ≥ 44pt × 44pt. Visible target can be smaller (24pt icon) but the tappable zone must be 44pt. Use padding to expand.
- Dates are relative when fresh, absolute when old.
2h ago/Yesterday/Mar 12. Never2026-03-12T14:36:42Zin UI. - Bottom tab bar is fixed: 5 tabs, never reorder. Home · My learning · Library · Events · Account. Bottom tabs are app-level navigation, not contextual.
Typography — mobile ramp
Same typeface (Lato) and same token names as web, but headings shrink. Reading distance on mobile is ~25-35cm vs 50-70cm desktop, so 32px H1 already feels large in hand.
| Token | Web | Mobile | Notes |
|---|---|---|---|
--kcds-text-display | 80px | 34px | Rare on phone; hero/splash only |
--kcds-text-h1 | 32px | 24px | Page titles, screen headers |
--kcds-text-h2 | 24px | 20px | Section headers, card headers |
--kcds-text-h3 | 20px | 17px | Sub-headers, list group headers |
--kcds-text-body | 16px | 16px | No change — mobile body floor (iOS auto-zoom prevention) |
--kcds-text-caption | 12px | 12px (13px when primary content) | Bump to 13 when caption IS the row content (audit timestamps) |
The 16px body floor is absolute. iOS Safari auto-zooms any input with
font-size < 16px on focus, breaking layout. Body and form inputs always 16px.
Dynamic Type (iOS) and Font Scale (Android)
KC apps respect user font-size accessibility settings. Use minHeight not
height on text rows. Test at AX5/AX6 — fixed-height rows clip at largest sizes.
Hit areas
The visible target and the tappable zone can differ. Apple HIG and Material both call for 44pt × 44pt minimum tappable area, regardless of how small the visible icon is.
Icon is 24pt for visual proportion, but the tappable area is 44pt — expanded with transparent padding around the icon. Users with low fine-motor control can still hit it; everyone else just doesn't notice.
Common patterns:
- Top app bar icons: 24pt icon centered in 44pt × 44pt button
- Tab bar items: full column tap area (~75pt × 56pt), icon centered
- List row trail icons: stretch the tap zone to row height (≥56pt)
- Card chevrons: tap zone extends across the entire row, not just the icon
Spacing
Mobile uses the same spacing scale as web — 4 / 8 / 12 / 16 / 20 / 24 / 32 / 40 / 48. But mobile uses smaller values more often, because viewport is narrow.
Standard screen padding
| Screen edge | 16px horizontal (never less) |
| Section gap | 24px between major sections |
| Card gap | 12px between cards in a list |
| Row vertical padding | 12px top/bottom on list rows |
| Touch gap | 8px minimum between adjacent tap targets |
Density
Mobile has only comfortable density — there is no compact mode. Compact density on web exists for data-heavy admin surfaces (LMS rosters, etc.) where users sit at desks and scan dense tables. On mobile, the smaller viewport and touch targets make compact untenable.
The LMS Admin mobile app is the closest case to "admin density", but even there touch targets stay at 44pt and rows stay at ≥56pt height. The compactness comes from showing fewer items per screen, not from shrinking each item.
Dark theme
Dark theme is opt-in via [data-kcds-theme="dark"] on root. The dark palette is
near-black neutral, not the warm #111827 gray-blue used in Tailwind defaults.
Try the Theme toggle in the sidebar — every component on this page will switch.
Rule — light renders on first paint
The first frame of any generated KC page is always light, regardless of
OS preference. Designers reviewing previews on dark-themed machines must not get
dark-on-dark flash. Dark theme is explicit attribute, never auto-applied via
prefers-color-scheme.
Surface stack
| Surface | Light | Dark |
|---|---|---|
| Page background | #F3F7F9 | #0A0A0B |
| Card / sheet | #FFFFFF | #131316 |
| Elevated section | #FFFFFF | #1A1A1D |
| Track / progress | #EAEFF3 | #2A2A2D |
Product accents
Mobile inherits the same per-category accent system as web. Switch categories in the sidebar to see chrome change. Each product picks its category and that's the accent used everywhere — top bar, primary buttons, FAB, active tab.
| Product | Category | Primary |
|---|---|---|
| Learner Portal | Legacy | Orange #FF881A |
| LMS Admin | Legacy (TBD) | Orange #FF881A |
| Safety | KC Comply | Slate #475569 |
| Performance (planned) | KC Perform | Indigo #4F46E5 |
| USMP (planned) | Platform | Cyan #0891B2 |
Gestures overview
KC mobile uses platform-standard gestures. Don't invent new ones. Users come from other apps and expect familiar interactions.
| Gesture | Where | Action |
|---|---|---|
| Tap | Anywhere tappable | Primary action |
| Long press | List items, cards | Reveal context menu (rarely used in KC) |
| Swipe right→left | List rows | Reveal trailing actions (delete, archive) |
| Swipe down at top | Scroll containers | Pull to refresh |
| Edge swipe right | iOS only | Back navigation (system handles) |
| Swipe down | On a sheet | Dismiss sheet |
| Pinch zoom | Images, video | Allowed; not used in app chrome |
Pull to refresh
Available on any scroll container that shows lists fetched from the network — My learning, Library, Events, Inbox. Triggers a fresh fetch and replaces stale data.
- Spinner appears when user pulls down ~40px from scroll position 0
- Release with sufficient pull triggers fetch
- Use orange spinner (Learner Portal) or product accent
- Show for minimum 600ms even if fetch returns instantly — prevents flash
- On error, show a toast/snackbar, keep existing data visible
Swipe actions
Available on list rows when the row has a destructive or quick action that's faster than tapping into detail and finding a button.
- Swipe right→left reveals trailing actions (1-2 max)
- Destructive action (delete): red background, white icon
- Neutral action (archive, mark read): gray/neutral background
- Full swipe (past 60% width) commits the leading action
- Always provide an alternate tap path — swipe is a power-user shortcut, not the only way
- Add haptic feedback on action commit (success) and on cancel (warning)
Haptics
Use sparingly. Haptics are seasoning, not the main course. Trigger them on meaningful state changes only — never on every tap.
| Event | iOS pattern | Android pattern |
|---|---|---|
| Success (course completed) | UINotificationFeedbackGenerator success | HapticFeedbackConstants.CONFIRM |
| Error / warning | UINotificationFeedbackGenerator error | HapticFeedbackConstants.REJECT |
| Selection change (picker, segmented) | UISelectionFeedbackGenerator | HapticFeedbackConstants.CLOCK_TICK |
| Light tap (toggle on) | UIImpactFeedbackGenerator light | HapticFeedbackConstants.VIRTUAL_KEY |
On Android API < 30, fall back gracefully — older devices have a smaller haptic vocabulary.
Keyboard avoidance
When the soft keyboard appears, content must not be obscured. iOS and Android handle this differently — but KC mobile guarantees the active input is always visible.
- iOS: Use
KeyboardAvoidingViewwithbehavior="padding"in React Native, or constrain layouts to the keyboard's safe area in native - Android: Default
adjustResizewindowSoftInputMode handles most cases. Test in landscape - Sticky CTA buttons rise above the keyboard when input is focused
- Auto-scroll the active input into view with 24px buffer above the keyboard top
- Tap outside dismisses keyboard except when typing into another input
M1 — Bottom tab shell
The primary shell. Every screen directly reachable from the bottom tab bar uses M1. Top app bar (56pt) + scrollable content + bottom tab bar (56pt + safe area).
Spec
| Top app bar height | 56pt + env(safe-area-inset-top) |
| Top bar background | White (default) or accent (Home only) |
| Content area | flex: 1; overflow-y: auto — scroll lives only here |
| Bottom tab bar height | 56pt + env(safe-area-inset-bottom) |
| Tabs | 5 fixed (Home, My learning, Library, Events, Account) — never reorder |
| Tab icon style | Thin outline, stroke-width: 2, never filled |
Critical CSS — phone container + scroll body
.phone {
width: 390px;
height: 100dvh; /* ← height, NOT min-height */
overflow: hidden; /* prevents content overflow */
display: flex; flex-direction: column;
}
.top-bar {
flex-shrink: 0; /* never compress */
position: sticky; top: 0; z-index: 50;
}
.scroll-body {
flex: 1; /* take all remaining space */
overflow-y: auto;
padding: 16px 16px calc(var(--tab-height) + var(--safe-bottom) + 16px);
}
Common mistake: min-height: 100dvh on .phone → parent stretches to content → flex: 1 on scroll-body has no ceiling → no scrolling.
M2 — Single flow shell
Detail screens and deep flows. User navigated here from M1 (tapped a course, event, assignment). Back arrow returns to parent. Tab bar is hidden.
Q2 Compliance training
Required pathway for all employees. Covers GDPR, data handling protocols, and Q2 policy updates from the legal team.
Spec
| Top app bar | Back arrow (leading) + centered title + max 2 trailing icons |
| Back arrow hit area | 44pt × 44pt; icon-only, no text label |
| Title | 17pt medium, sentence case, ellipsis if long |
| Hero image (optional) | Full viewport width, ~200pt tall, 16:9 ratio |
| Sticky bottom CTA | Primary action (e.g. Continue, Submit), full width, 56pt + safe area |
| Bottom tab bar | Hidden in M2 |
M3 — Anchored sheet
Bottom sheet that slides up from below. Used for filters, action choices, contextual editing. Backdrop dims the underlying page; sheet is dismissible via swipe down, backdrop tap, or X button.
Course type
Spec
| Sheet width | Full viewport (max 430px on desktop preview) |
| Top corner radius | 24px (top only, bottom is screen edge) |
| Max height | 92dvh — leaves ~8% backdrop visible above |
| Drag handle | 36×4px, border-radius: 9999px, centered, 12px from top |
| Backdrop | rgba(0,0,0,0.45) |
| Dismiss | Swipe down · backdrop tap · X button |
| Apply button | Top-rounded only (24px 24px 0 0), 64pt tall, sticky bottom |
M4 — Conversational
AI chat / Q&A surfaces. Used by the in-app AI assistant. Distinct from M1/M2 because the input bar is sticky at bottom (above keyboard) and the message stream is the primary content. Used in Learner Portal AI tutor and LMS Admin AI assistant.
Spec
- Top app bar: assistant name + clear button (trash icon)
- Message stream: alternating user (right, accent bg) and assistant (left, surface) bubbles
- Auto-scroll to latest message on send
- Input bar: 56pt height, sticky bottom + safe area, mic + send buttons
- Citations: inline numbered footnote links → tap opens M3 sheet with source
- Typing indicator: three-dot animation while assistant generates
Top app bar
56pt height + safe-area-inset-top. Two variants: M1 (no back arrow, optional logo) and M2 (back arrow + centered title + max 2 trailing icons).
When to use each variant
| M1 accent | Home tab only. Welcome message, brand presence. |
| M1 white | My learning, Events, Library tabs. Title + filter/search icons. |
| M2 centered | Detail screens. Back arrow leading, title centered, 1-2 trailing actions. |
Buttons
56pt height (full width or pill), 28pt corner radius, 16pt body text weight 400. Larger and thicker than web buttons because they're thumb-driven.
| Variant | Use case |
|---|---|
| Primary (filled) | The main action of the screen. One per screen. |
| Secondary (outline) | Alternative action equally available alongside primary. |
| Tertiary (text) | Low-stakes actions: cancel, dismiss, more info. |
| Destructive | Deletion or unrecoverable action. Always confirm via modal. |
Bottom tab bar
Fixed at the bottom of every M1 screen. Five tabs, never reorder. Active state = color change only (icon + label both turn accent). Icon outline style never fills.
| Height | 56pt + env(safe-area-inset-bottom) |
| Tab count | 5 fixed |
| Icon | 24pt outline, stroke-width 2 (never change between states) |
| Label | 10-11pt, sentence case, weight 400 in both states |
| Active | Icon + label both turn accent color |
| Inactive | Both gray (text-muted) |
| Border | 1px top border, subtle |
Search bar
56pt height — same as CTA buttons. Never 44pt and never pill-shaped. Sits at top of Library and Account tabs (no top app bar above search).
| Height | 56pt (same as CTA buttons, never 44pt) |
| Corner radius | 12pt (moderate, NOT pill) |
| Border | 1px subtle |
| Background | Surface default (white) |
| Leading icon | 18pt search, text-tertiary color |
| Font size | 16pt (iOS zoom prevention floor) |
List rows
Workhorse of mobile UI. Used for every list — courses, assignments, notifications, settings. Min 56pt height to maintain 44pt hit area. Leading icon, title, optional subtitle, trailing chevron.
Spec
| Min height | 56pt |
| Horizontal padding | 16pt |
| Leading element | 36×36 circle (avatar, icon, status) — 14pt gap to body |
| Title | 16pt, weight 600 |
| Subtitle | 13pt, text-tertiary |
| Trailing | Chevron right or value text + chevron |
| Divider | 1px subtle, between rows (not after last) |
Floating Action Button (FAB)
Anchored bottom-right above the tab bar. Used for the screen's primary creation action when it deserves persistent prominence — e.g. "Report incident" in Safety, "Ask AI" in Learner Portal.
One FAB per screen max. Position 16pt from screen edges, above the tab bar. Size: 56×56 circle. Background: accent. Icon: 24pt white.
Modal
Center-screen alert. Used only for decisions the user must make before continuing — confirmation of destructive actions, account warnings. Two actions max.
Delete account?
This will permanently remove your account and all course progress. This action cannot be undone.
Action sheet
A smaller variant of the M3 sheet — used for short lists of contextual actions (e.g. "Share", "Edit", "Delete"). Slides from bottom, doesn't take full height. See M3 shell for the filter-style sheet pattern.
- Used for ad-hoc actions on a specific item (tap-and-hold or ⋯ menu)
- Title at top, list of actions below, Cancel at bottom (iOS pattern)
- Destructive actions: red text
- Dismiss via swipe down or Cancel
Snackbar / Toast
Brief message at the bottom of the screen. Used for confirmations and non-critical errors that don't block the user. Auto-dismisses after 4 seconds.
Position: 76pt from bottom (above tab bar). Auto-dismiss 4s. Single optional action. Dark background for contrast against any page surface.
Banners — offline, access-locked
Persistent system-state messages that sit below the top app bar. Two main variants: Offline (network down) and Access locked (subscription / tenant restriction).
Network status banner
Full-width persistent banner anchored directly above the tab bar. Appears automatically when connectivity changes. Two states: Not connected (red) and Connected (green). "Connected" auto-dismisses after 3s.
Not connected
Connected (auto-dismisses 3s)
| Not connected | Red #EF4444 · wifi-off icon · persists until restored |
| Connected | Green #22C55E · wifi icon · auto-dismisses after 3s |
| Position | Fixed, directly above tab bar · no border-radius · full width |
| Height | 48pt · icon + label centered |
Updating state
Indicates background data refresh on screens like My Progress. Non-blocking — content stays visible and interactive. Peach strip below top bar. Two variants: pill label or spinner.
Variant A — pill
Variant B — spinner
| Strip background | Peach --kcds-color-primary-tint |
| Variant A | White pill, "Updating", 14pt regular — default |
| Variant B | Orange spinner — when update takes >2s |
| Content | Fully visible and interactive — not dimmed, not blocked |
Downloads screen
M2 screen. Accessible from My learning → Downloads → "Show all". Lists downloaded courses with real thumbnails, storage info, per-item delete, and "Delete all" sticky CTA (peach fill).
| Shell | M2 — back arrow, no tab bar |
| Thumbnail | Real photo (not gradient placeholder) — object-fit: cover |
| Delete icon | trash-2, 18pt, accent color · shows confirmation modal |
| Downloading | Orange spinner replaces trash icon · same card layout |
| Delete all CTA | Peach fill, dark text, sticky bottom — same as "Cancel request" |
Active filters
When filters are applied: filter icon gets an orange dot badge, a removable chips row appears below the toolbar. Search replaces the topbar. Empty state text adapts to the cause.
Filters active — no search
Search + filters — no results
Search only — no results
| Filter icon dot | Orange dot 8pt, top-right of icon box, white 2px gap — shown only when ≥1 filter active |
| Chips row | Appears when ≥1 filter active · each chip: label + × · horizontal scroll · never wraps |
| × removes | One filter only — NOT all filters at once |
| Search = topbar replacement | Entire topbar replaced by search input + "Cancel" button (orange text) |
| "Clear search and filters" CTA | Shown ONLY when BOTH search query AND filters are active simultaneously |
| Empty state copy | Adapts to cause: filters only / search+filters / search only — three distinct messages |
Empty states
Used when a list, page, or section has no data to display yet. Always include an icon, a title (Title Case), short explanation, and an action when one is possible.
No Courses Yet
Browse the Library and bookmark courses to start learning.
Course card
Full-width vertical card used in Library catalog. Image cover on top, body with title, metadata, status. Vertical not horizontal — Library was redesigned to vertical cards for better thumb-scrolling and visual breathing room.
| Cover image | Full width × 140-200pt height |
| Card | 1px border-default, 16pt corner radius, no shadow |
| Body padding | 12pt vertical, 16pt horizontal |
| Type label | 13pt text-tertiary |
| Title | 17pt weight 700, line-height 1.3 |
| Meta row | 13pt text-tertiary: level · runtime · author |
| Status row | 14pt weight 600: icon + label, color per state |
Learner home
Home tab (M1). Orange hero banner with greeting and bell. Sections: Continue learning (horizontal scroll cards), My progress (donut + details), Recommended (list rows), Upcoming events. 5-tab bar.
Library
Library tab (M1). No top app bar — search is the header. Tint gap + big white block pattern: orange-tint page, white rounded block with category chips, filter row, and vertical course cards.
My learning
My learning tab (M1). Orange hero with greeting + horizontal scroll assignment cards. Below hero: rounded white sections for Continue learning, My progress (donut), Recently viewed hscroll. No tint page — white background, orange hero.
Dropdown / cycle selector
Used on My Progress and similar screens. Tapping the calendar icon or group name opens a dropdown list below. Two variants: date cycle selector (pick a period) and group selector (switch training group).
Cycle selector (dates) — open
Group selector — open
| Trigger | Calendar icon (left) + label + chevron (right). Chevron up = open, down = closed. |
| Dropdown | White card, border-radius: 14px, subtle shadow, rows divided by 1px separator |
| Active item | Peach fill --kcds-color-primary-tint, no checkmark needed |
| Inactive items | White bg, sentence case, font-weight: 400 |
| Dismiss | Tap outside or tap active item again |
Page banner
Full-width promotional or motivational banner. Edge-to-edge, no side margins, rounded corners. Left: icon + title + subtitle. Right: decorative illustration. Background can be any brand color — not just orange.
| Width | Edge-to-edge — no horizontal margin. border-radius: 16px all corners. |
| Left zone | Icon (32×32, soft bg square, 8px radius) + title (700) + subtitle (400) |
| Right zone | Decorative illustration or icon placeholder, 70×70, border-radius: 12px |
| Backgrounds | Primary color (solid), white/neutral (with border), dark. Avoid primary-tint as banner bg — it looks wrong with non-orange accents. |
| CTA | Separate full-width button below banner — not inside it |
Course detail — Instructor-led / Webinar
Course detail (M2). Hero thumbnail, content type label + inline type badge, status badge, sticky CTA. Sub-tabs: Events (active) / Overview / Files. Events tab shows live event card with meta table and "Join the class" CTA.
| Content type label | "Course: Instructor-led" text + inline Webinar badge (orange fill, white text, UPPERCASE, border-radius: 4px) |
| Sub-tabs | Events / Overview / Files — horizontal scroll if needed. Events tab shows the event card. |
| Event card badge | Neutral surface-canvas background, dark text, UPPERCASE — NOT orange fill |
| Live now | Pulsing orange dot + "Live now" text, orange, font-weight: 700 |
| Meta table | 2-column grid: label (tertiary, 90px) + value (primary). font-weight: 400 on values. |
| CTA | "Join the class" — primary orange. Context-aware: "Show certificate" when passed, "Join the class" when live. |
| Tab bar | Hidden — M2 shell, user navigated into course from a tab |
Account
Account tab (M1). No top app bar — avatar + name acts as the header. Mode B: orange-tint page background. White edge-to-edge cards float on tint. Icons bare accent-color (no container box).
Event information
Event detail (M2). Page-bg background. ONE white edge-to-edge card with all content: event badge (UPPERCASE), title, meta table, Cancel request (neutral surface fill, dark text), approval chain. Back arrow is dark — not orange. Tab bar hidden.
Sub-tabs / Segmented control
Horizontal row of equal-width tabs used to switch between views inside a screen. Not the shell tab bar — sub-tabs live inside a page section. Used in My learning (Current / Upcoming / Past), Events detail, course detail (Overview / Content / Reviews), LMS Admin lists.
| Property | Value |
|---|---|
| Layout | display: flex — each tab flex: 1 (equal width). Never wrap to second line. |
| Height | 56px — same as CTA buttons. Never 40px or 48px. |
| Border radius | 12px — NOT pill (9999px) |
| Font size / weight | 16px / 400 (Regular) — never bold |
| Active tab | background: var(--kcds-color-primary-tint) + border: 1px solid transparent (NO visible border on active) |
| Inactive tab | background: var(--kcds-color-surface-default) (white) + border: 1px solid var(--kcds-color-border-default) |
| Overflow rule | If tabs don't fit: overflow-x: auto + flex-shrink: 0 on each tab. Use flex: 1 ONLY when count is fixed and labels are short. |
.tab-btn {
display: inline-flex; align-items: center; justify-content: center;
padding: 0 20px;
height: 56px; /* same as CTA buttons — always */
border-radius: 12px; /* NOT pill, NOT 8px, NOT 16px */
font-size: 16px;
font-weight: 400; /* Regular — never bold */
white-space: nowrap; cursor: pointer; flex-shrink: 0;
}
.tab-btn--active {
background: var(--kcds-color-primary-tint);
border: 1px solid transparent; /* NO visible border on active */
color: var(--kcds-color-text-primary);
}
.tab-btn--inactive {
background: var(--kcds-color-surface-default);
border: 1px solid var(--kcds-color-border-default);
color: var(--kcds-color-text-primary);
}
flex-wrap: wrap (tabs never wrap) · font-weight: 700 on tab labels · visible border on active tab.
Page composition
Rules for structuring a mobile screen: scroll container, section cards, horizontal rows, and drag-to-scroll. Based on approved canonical screens (My Learning, USMP Skills catalog).
Scroll pattern
Scroll works through body, not an inner container. min-height: 100vh on .phone lets content grow beyond the screen; body scrolls naturally.
.phone {
width: 390px;
min-height: 100vh; /* NOT height — body scrolls */
background: var(--bg-canvas);
}
.scroll {
padding: 12px 0;
padding-bottom: calc(var(--tab-height) + env(safe-area-inset-bottom) + 12px);
display: flex; flex-direction: column; gap: 0;
/* NO overflow-y — body scrolls */
}
Section cards
Each logical section is a separate white card. Cards are edge-to-edge — no horizontal margins. An 8px gap between cards reveals the canvas background as a visual separator.
.card {
background: var(--bg);
border-radius: 16px;
border: 1px solid var(--border);
overflow: hidden;
margin-bottom: 8px;
/* NO margin-left/right — edge-to-edge */
}
.card-header {
display: flex; justify-content: space-between; align-items: center;
padding: 16px 16px 12px;
}
.card-title { font-size: 17px; font-weight: 700; }
.card-link { font-size: 13px; color: var(--kc-accent); font-weight: 400; }
Horizontal scroll rows
Horizontal content rows (skill cards, thumbnails, category pills) use drag-to-scroll JS — required for mouse/trackpad users.
.hrow {
display: flex; gap: 12px; overflow-x: auto;
padding: 0 16px 14px;
cursor: grab;
}
.hrow::-webkit-scrollbar { display: none; }
/* Drag-to-scroll JS (required) */
function drag(el) {
let down=false, sx, sl;
el.addEventListener('mousedown', e=>{
down=true; el.classList.add('drag');
sx=e.pageX-el.offsetLeft; sl=el.scrollLeft; e.preventDefault();
});
el.addEventListener('mouseleave',()=>{ down=false; el.classList.remove('drag'); });
el.addEventListener('mouseup', ()=>{ down=false; el.classList.remove('drag'); });
el.addEventListener('mousemove', e=>{
if(!down)return;
el.scrollLeft=sl-(e.pageX-el.offsetLeft-sx)*1.2;
});
}
document.querySelectorAll('.hrow').forEach(drag);
margin: 0 16px on cards · overflow-y: auto + height: 100vh on .phone · all sections in one big card · font-weight: 700 on buttons (only on headings) · sections without a card wrapper.
Page backgrounds
KC mobile uses two background modes. The product and screen role determine which to use.
Plain white page. Cards have border + optional box-shadow.
Use for: content screens, dense lists, library category, course detail, admin tables.
Accent-tint background. White cards edge-to-edge, NO border, NO shadow — tint gap separates.
Use for: Account, settings, profile, utility hubs.
Mode B — border-radius rules
| Tint above card? | Tint below card? | border-radius |
|---|---|---|
| Yes | Yes | 20px (all corners) |
| No | Yes | 0 0 20px 20px |
| Yes | No | 20px 20px 0 0 |
Dark theme
Both modes flip in dark theme. Mode A: --bg → near-black #0A0A0B. Mode B: --kc-accent-light → #0F0F11. Pattern logic identical — only colors change. Always activated by [data-theme="dark"], never by prefers-color-scheme media query.
.scroll for a Mode B panel (creates white strip) · auto-applying prefers-color-scheme: dark.
Iconography
KC mobile uses Lucide icons exclusively. Library pinned at @0.460.0 — same as web. All icons SVG outline style.
Size table
| Context | Size |
|---|---|
| Tab bar icons | 24px |
| Top app bar actions | 20px |
| List row leading icon | 18px |
| Badge / count | 12–14px |
| Category icon (in cards) | 22px |
| Card thumbnail icon | 28–32px |
| Inline / body text icon | 16px |
Top app bar padding rule
Top bar icon alignment requires explicit margin — don't rely on parent padding alone.
.topbar__title {
margin-left: 16px; /* explicit — don't rely on parent padding */
}
.topbar__actions {
margin-right: 8px; /* explicit — keeps action icons in position */
display: flex; gap: 4px; align-items: center;
}
Account tab icons — no container box
Icons in the Account tab list rows are plain accent-color SVG — no background container box. Differs from other contexts where icons have a tinted square/circle container.
Available on web
When a section exists on web but is too complex for mobile — don't hide it from navigation. Show a dedicated screen that tells the user where to go. Positive framing: available on web, not not available on mobile.
.web-only-screen {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center;
padding: 40px 32px; gap: 16px; text-align: center;
}
.web-only-icon {
width: 64px; height: 64px; border-radius: 16px;
background: var(--kc-accent-light);
display: grid; place-items: center;
color: var(--kc-accent);
}
.web-only-title { font-size: 20px; font-weight: 700; }
.web-only-desc { font-size: 15px; color: var(--text-secondary);
line-height: 1.5; max-width: 260px; }
LMS Admin uses this pattern for: Reports builder, Certificate templates, Account settings, Category manager, Communication center, TNA, Question bank, Import data.
Rule: the section stays in navigation (More / sidebar) — it's never hidden. The user knows the feature exists and where to find it.
Activity feed
Chronological log of learning actions with points earned. Used on My Progress → My points tab. Groups events by date with group headers.
| Element | Spec |
|---|---|
| Date header format | "Today · August 5" / "Friday · August 2" |
| Date header | 14pt regular, text-tertiary color |
| Event icon | 20px, type-specific color (accent or green for events) |
| Event type label | 14pt 500, same color as icon, sentence case |
| Content name | 14pt regular, black, underlined (tappable) |
| Points | 14pt 500, accent color, trailing, "+ N pts" |
| Row min-height | 56pt |
| Row separator | 1px rgba(0,0,0,0.06) |
Approval chain
Visualizes a multi-step approval workflow for event registration. Appears on Event Information screen (M2) when an event requires manager/HR approval. KC-unique — no web equivalent.
| State | Icon | Color |
|---|---|---|
| Approved | circle-check | Green #22C55E |
| Pending — current | clock | Gray #9CA3AF |
| Next — locked | lock | Light gray #D1D5DB |
| Rejected | circle-x | Red #EF4444 |
Cancel request button (when status is Pending): peach tint fill — background: var(--kc-accent-light), dark text, NO red, NO outlined destructive. Height 56pt, full width, border-radius: 24px, font-weight: 400. Scrolls with content, requires confirmation dialog.
Course accordion
Shows course content structure: modules with collapsible lesson lists. Used on Course detail → Content tab.
| Element | Spec |
|---|---|
| Module title | 15pt / 500 / primary |
| Module meta | "Lessons: N · Xm Xs" · 12pt regular · tertiary |
| Module row height | 56pt |
| Chevron | ∧ expanded / ∨ collapsed · 20pt · tertiary |
| Lesson title | 14pt regular · primary |
| Lesson duration | 12pt regular · tertiary |
| Lesson row height | 56pt |
| Expand animation | 200ms smooth |
| Status — done | circle-check · 18px · green #22C55E |
| Status — not done | circle · 18px · light gray #D1D5DB |
Lesson strip
Horizontal strip of lesson thumbnails at the top of the video player screen. Shows all lessons in the current module with completion status. Allows quick navigation between lessons.
| Property | Value |
|---|---|
| Thumbnail size | 48×48pt |
| Corner radius | 8pt |
| Completion badge | Green circle-check overlay, bottom-right |
| Current lesson indicator | Accent-color underline, 4pt height, below thumb |
| Strip height | 72pt (48pt thumb + 12pt padding each side) |
| Scroll | Horizontal, no visible scrollbar |
Offline banner
Indicates a course is available offline. Appears on Course detail → Content tab above the course accordion. Tappable — navigates to Account → Downloading settings.
| Property | Value |
|---|---|
| Background | Accent at 5% opacity |
| Icon | cloud-download · 20px · accent color |
| Text | "Course is available offline" · 14pt regular · accent color · sentence case |
| Trailing | chevron-right · 16px · accent color |
| Height | 48pt |
| Corner radius | 12pt |
| Tap destination | Account → Downloading settings |
When to show: course marked as downloadable by admin AND at least one lesson is available offline. Show "Download for offline" CTA instead if no content is downloaded yet.
Typography — link rules
Underline = tappable link. Always. No exceptions. Never use text-decoration: underline for visual decoration — if text is underlined, it must have a tap handler and destination.
| Link type | Style |
|---|---|
| Inline tappable (course name, event name) | Underlined · color: var(--text-primary) · NOT blue · NOT orange |
| "Show all" / navigation prompts | No underline · color: var(--kc-accent) · font-weight: 400 |
| Destructive link (e.g. remove item) | color: var(--kcds-color-status-error) · no underline |
/* Tappable link — body text color, underline only */
.link {
text-decoration: underline;
text-underline-offset: 2px;
color: var(--text-primary); /* NOT blue */
cursor: pointer;
}
/* "Show all" / section action */
.link--action {
color: var(--kc-accent);
text-decoration: none;
font-weight: 400;
}
Horizontal scroll
overflow: hidden on any ancestor of a horizontal scroll container will silently clip the content and prevent swiping. This is the single most common cause of broken carousels on mobile previews.
overflow: hidden is only allowed on leaf nodes that contain images/media. Never on layout wrappers, page containers, sections, or any ancestor of a scroll row.
/* WRONG — any of these breaks horizontal scroll */
.phone { overflow-x: hidden; }
.hero { overflow: hidden; }
.section { overflow: hidden; }
/* CORRECT — overflow:hidden only on leaf image containers */
.card { overflow: hidden; } /* clips thumbnail to rounded corners */
.thumb { overflow: hidden; }
/* Correct horizontal scroll */
.hscroll {
display: flex; gap: 12px;
overflow-x: auto;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
margin-right: -16px; /* bleeds to right edge */
padding-bottom: 4px;
cursor: grab;
}
.hscroll.dragging { cursor: grabbing; user-select: none; }
.hscroll::-webkit-scrollbar { display: none; }
.hscroll > *:last-child { margin-right: 16px; }
.hscroll > * { flex-shrink: 0; } /* items never compress */
Drag-to-scroll is required on ALL overflow-x: auto rows — not just course cards, but also category chips, filter chips, and any other horizontal row. Without it, mouse/trackpad users can't scroll.
function enableDragScroll(el) {
let isDown = false, startX, scrollLeft;
el.addEventListener('mousedown', e => {
isDown = true; el.classList.add('dragging');
startX = e.pageX - el.offsetLeft; scrollLeft = el.scrollLeft;
e.preventDefault();
});
el.addEventListener('mouseleave', () => { isDown = false; el.classList.remove('dragging'); });
el.addEventListener('mouseup', () => { isDown = false; el.classList.remove('dragging'); });
el.addEventListener('mousemove', e => {
if (!isDown) return;
el.scrollLeft = scrollLeft - (e.pageX - el.offsetLeft - startX) * 1.2;
});
}
// Apply to ALL horizontal scroll rows
document.querySelectorAll('.hrow, .hscroll, .cats, .chips').forEach(enableDragScroll);
Typography weight rule
KC mobile uses only two weights: 400 (Regular) and 700 (Bold). 600 (SemiBold) is not used.
- Course titles, event titles (main heading of a card)
- Page / screen section headers (h1, h2 structural headings)
- Badges (event type, status badges)
- Status text ("Pending", "Approved", "Live now")
- Meta values (Instructor, Runtime, Remaining seats)
- Tab labels, button labels, nav labels
- Settings row labels, filter chips
- "Today" button, "Show all" links
- All secondary / helper text
text-transform: uppercase · letter-spacing: .04em · font-weight: 400 · always uppercase regardless of source data. Background: var(--kcds-color-primary-tint) (NOT gray surface-canvas) · border-radius: 8px (NOT pill) · border: none · font-size: 11px.
Content type labels
All content must show a type prefix so users know what they're looking at before tapping in. Never drop the prefix to save space.
| Type | Label format |
|---|---|
| On-demand course | Course: On-demand |
| Instructor-led course | Course: Instructor-led |
| Instructor-led + Webinar | Course: Instructor-led · Webinar |
| Instructor-led + Zoom | Course: Instructor-led · Zoom |
| Learning path | Learning path |
| Skill path | Skill path |
Mobile vs web — delta table
Where mobile diverges from kc-design-foundations (web). All other rules are identical — cross-link to web skill rather than re-documenting.
| Topic | Web (kc-design-foundations) | Mobile (kc-ds-mobile) |
|---|---|---|
| Density | Comfortable / Compact | Comfortable only — no compact |
| Hit area floor | 44×44 CSS px, pointer:coarse | 44pt always — no media query |
| Navigation | Pattern A/B + hamburger | M1 bottom tab + M2 back stack |
| Hamburger drawer | Default mobile transformation | Web-only, not in native apps |
| Motion duration | 200ms default | 150ms default |
| Motion style | CSS cubic-bezier | Spring (translate) + timing (opacity) |
| Haptics | N/A | Defined: light / medium / heavy / success / warning / error |
| Sheets | Optional pattern | Primary pattern for overlays |
| Typography headings | Full scale (H1 up to 80px) | Reduced scale (H1 max 28–32pt) |
| Bottom tab bar | Optional for some products | Always 5 tabs, fixed order |
| Dark theme activation | data-kcds-theme="dark" | Same — data-theme="dark". Never auto prefers-color-scheme. |
| Scrollbar styling | Themed via CSS scrollbar-color | Hide all scrollbars (display:none) |