Back
Design SystemSaaS Platform

Introduction

2424r 1226.6°1 : 24.444.443.844.36

Appsatile is a business operating system that replaces 15+ disconnected tools with a single, unified workspace, where teams run daily operations and build new apps within the same environment.

This design system was built to support multiple integrated apps and hold many surfaces together through a flexible theming architecture, all on a WCAG-compliant, multi-layer token foundation designed to scale while keeping the experience consistent.

My roleDesign System Architecture & Accessibility
TeamEshaan Abhyankar, Daria Zoria, Chinonso Ezeigbo
FormatTeam SaaS design system
ToolsFigma

A token-based foundation, covering colour, typography, spacing, and radius, gives every future screen and app built on Appsatile the same visual language from day one, rather than each new surface improvising its own as the platform grows.

Design tokens1,100+

Three-tier architecture – Primitive, Semantic, and Component-level – covering colour, typography, spacing, radius, elevation, and density.

Variable collections15

Spanning colour modes, density scales, typography, iconography, and shape – designed for scalable theming and cross-product flexibility.

Components70+

From atoms to organisms – inputs, navigation shells, modals, and data views – built for consistency, accessibility, and cross-product integration.

Icons868

9 Weights, 9 Sizes, Variable Controlled

Density modes04

SM, MD, LG, XL - All via variable modes

AccessibilityWCAG

Compliant contrast at every density.

Colour System

Colour is built in two layers, and components only ever touch the second one. The base layer is raw value with no meaning attached: three neutrals, four primary hues, and fourteen extended hues, each one a full 13-shade scale from a near-white 25 to a near-black 1000. None of it says what it's for yet.

Brand is a primitive alias, not a colour of its own — it points at whichever primary hue is the product's actual brand colour, and every derived and semantic token points at Brand rather than at that hue directly. Change what Brand aliases to, and the update cascades through the entire system on its own.

Neutrals39

3 hues
13 shades each

Primary52

4 hues
13 shades each

Extended182

14 hues
13 shades each

White
#FFFFFF13.48 AAA

Alpha

Alpha ramps aren't new colours — they're the same base colour (Grey, White, Black, or Brand) shown at different levels of transparency, so each one blends into whatever surface sits behind it instead of being a flat, fixed swatch. That's what makes them right for things like hover states, borders, and shadows: instead of one fixed value, they adapt to whatever's underneath, light or dark.

Alpha: 38%
Alpha / DarkBase: #4D4D4C
  • Text & icons: readable overlays on light surfaces.
  • Interaction states: hover, pressed, disabled.
  • Borders & dividers: subtle separators for light surfaces.

Colour Semantics

None of this reaches a component directly. A second, semantic layer sits between the palette and the interface, naming each token by what it's for rather than what it looks like: a category (action, text, border, background, status), followed by an intent (primary, secondary, destructive, inverse, error), and, only where a value genuinely shifts on interaction, a state like hover or disabled. Six such groups (Text, Background, Action, Status, Border, and Depth) add up to 248 tokens, and every component references one of them rather than a raw hex value, so retheming the product is a matter of updating tokens once, not hunting through screens.

Colour System Tokens
Text14 tokens

Content colour by contrast. Inverse is fixed.

Background10 tokens

Content surfaces from canvas to modals.

Action27 tokens

All interactive states for controls.

Status20 tokens

System feedback.

Border11 tokens

Dividers, structure, and focus.

Depth4 tokens

Opacity scale for layers and scrims.

Token Naming Convention
What it stylesCategory

Action, Text, Border, Background, or Status.

What it's forIntent

Primary, Secondary, Destructive, Inverse, or Error.

InteractionState

For interactions like hover or disabled.

PropertyTokenValue
Backgroundaction/primary/default#2988FF
Labeltext/inverse/light#FFFFFF
Icontext/inverse/light#FFFFFF
Border rightborder/lightrgba(255, 255, 255, 0.24)

Modes

Every semantic token resolves to a different primitive depending on the active mode, so switching a screen from light to dark is never a second design. It's the same components, reading the same tokens, against a different set of values underneath.

Appsatile in dark mode
Appsatile in light mode
LightDark

Drag the line, or flick it. Same screen, same data, only the mode changed.

Typography

Type is built the same way colour is, in two layers. The primitives are a typeface, a Type Scale, and a small set of weights and tracking values. None of it says what it's for yet. That's what the named styles two subsections down are for.

Font FamilyInter

ABCDEFGHIJKLMNOPQRSTUVWXYZ

abcdefghijklmnopqrstuvwxyz

0123456789

!@#$%&()_+-=[]{}|;:",./?<>©®™→←↑↓⌘⌥⇧

Font Primitives

One typeface carries the whole system: Inter, a variable font built for screens, with a tall x-height that keeps mixed-case and lower-case text legible even at the smallest sizes the scale reaches. Everything below, weight included, is this same face.

Aa Bb Cc

Click to type

Type Scale

Ten steps, each pairing a size with the line-height that keeps it readable, run underneath every text style in the product. Weight and tracking are the other two primitives text draws from, four values each, so headings track tighter than body copy without either needing a bespoke figure.

Scale: 20px

Body base, Body large

Do good work.

PropertyValue
WeightMedium · 500
Size400 · 20px
Line height28px
Letter spacingNormal · 0%

Text Styles

None of this reaches a screen directly either. h1 through h6, the nine Body variants, and two Caption weights are just fixed picks off the scale above, named for what they're for rather than carrying their own size and line-height.

Do good work.

PropertyValue
Styleh1
Size40px
Line height48px
Weight700
Scale step900

Icons

Default: 1.2px

Icons are built on the same idea colour and type both are: a small set of primitives, not a value chosen by eye per instance. Here that's size and stroke weight, and beside text, both are computed from whatever the type scale gives that text rather than tuned on their own.

An icon's stroke is never set on its own. It's a fixed ratio of the icon's own size, computed rather than picked per instance, so a 16px icon and a 48px icon share the same proportion instead of one reading thin and the other heavy.

GeneralActionsFilesEditorCommunicationNavigationUsersLayoutMediaAnalyticsSecurityTimeArtificial IntelligenceTasksCloud StorageHuman ResourceDevelopmentCompaniesExtended Nav SectionGeneralActionsFilesEditorCommunicationNavigationUsersLayoutMediaAnalyticsSecurityTimeArtificial IntelligenceTasksCloud StorageHuman ResourceDevelopmentCompaniesExtended Nav Section
AccountingLeasingWorkforcePeopleNo CodeEmailCryptoDesignPayrollAppraisalPaymentsDirectoryPortfolioDocumentsCalendarEncryptionAssetsERPAccountingLeasingWorkforcePeopleNo CodeEmailCryptoDesignPayrollAppraisalPaymentsDirectoryPortfolioDocumentsCalendarEncryptionAssetsERP
ArrowsTabsProfileNav SectionScheduleSyncBookingsInboxSettingsTop BarPilotMain Content AreaCRMDev OpsReal EstateCyber SecurityMeetingsPerformanceArrowsTabsProfileNav SectionScheduleSyncBookingsInboxSettingsTop BarPilotMain Content AreaCRMDev OpsReal EstateCyber SecurityMeetingsPerformance

Icon System

One 24-unit grid and a single weight run across the library's 1,180 icons. Stroke width holds a constant ratio to size, 0.05, the same 1.2px this system states as its own default at 24px, so dragging through the scale never turns up a mismatched line weight at either end.

Size: 24px

24px · stroke 1.20px

Type-Icon Harmony

Beside text, an icon's own size and stroke are derived from the text style next to it, not set independently. Switch to Fixed to see why that matters: the same stroke value that reads fine next to Body all but disappears next to an h1, and turns heavy beside a caption.

Aa

Shown at 2× — real size below

Body · Font 14px → Icon 17px, stroke 0.85pxharmonised

Stroke weight and icon size are computed from the text token, not set independently. Selecting a text style, whether Helper Text or Hero Text, automatically resolves the correct icon specification. The result is a system where icons and type scale in unison, maintaining perceptual balance across every context, device, and accessibility zoom level without requiring manual intervention.

Spacing

Every value in this system is a mark on the same ruler: a multiple of 4px, plus two finer marks (2px and 6px) for the smallest gaps. Drag or fling the ruler below to move through them. On top of that ruler sits a second, plain-English layer: instead of picking 8px by hand, a component reaches for spacing-md. Change what spacing-md points to once, and everything using it updates together.

px
s-4
163248

Drag or use the arrow keys

Horizontal & Vertical

Spacing's semantic layer, built on Scale Base primitives, maps raw values to layout roles: gaps and padding. Selecting spacing-md always resolves to s-2, 8px, the same value shown below in the tag's own live gap.

Spacing: md
Design team
PropertyValue
Primitives-2
Semanticspacing-md
Value8px

For illustrative purposes only. Not rendered at true scale.

Overlap Spacing

The same primitives, negative: used when two elements should overlap rather than sit apart, like stacked avatars or layered cards.

Overlap: md
JDAKMPRS
PropertyValue
Primitives-neg-2
Semanticoverlap-md
Value-8px

For illustrative purposes only. Not rendered at true scale.

Scale Architecture

Colour, Typography, Icons, and Spacing each work with one property at a time. But a real component uses several of them together, and its height, padding, text size, and icon size only stay in proportion if something forces them to. Component Size is that force: one mode sets every part of a component at once, instead of each one by hand.

Setting each part by hand is risky. It's easy to miss one, and end up with a chip whose icon and size don't match. Those seven numbers also repeat everywhere the component is used, so changing them later means fixing every place by hand. One mode avoids this: change what a size means once, and every component using it updates together.

Component Size

Component Size

Four modes span Small to X-Large. Select one below, and every part of the chip changes together, not one at a time. Height comes from Spacing's own scale. Text and icon size follow Type-Icon Harmony's ratio.

Sky view.png
PropertyValue
Height40px
Padding4px / 10px
Gap8px
Border radius20px
Text style14px
Icon size17px
Stroke weight0.85px

Display Density

Radius

Radius Base is a set of raw numbers, from 0 to 999px. Semantic tokens like radius-md point to one of them. Change the raw number later, and the semantic token still means the same thing.

Radius follows size. Taller, more prominent elements get a bigger radius; small or compact ones get a smaller one. This keeps everything feeling consistent. Drag the box below and watch which step it lands in.

30px tall

Default. Form and UI elements from 28px (inputs, buttons, selectors).

PropertyValue
Stepmd
Primitiver-5
Semanticradius-md
Value6px

Border

Border weight has four tokens, from 0.5 to 3px: border-thin, border-regular, border-bold, and border-heavy. Each one is its own value, built for clarity and reuse rather than picked by hand each time.

Border weight shows how much attention an edge should get. A thin line fades into the background as a quiet divider; a bold one calls attention to a focus or selected state. Drag the slider below and watch which step the box lands in.

Border: 1px

Default border for most components.

PropertyValue
Stepregular
Primitivestroke-1
Semanticborder-regular
Value1px

Elevation

Elevation is two shadow layers stacked together: a thin hairline plus a soft, wide shadow that actually reads as lift. Semantic tokens like shadow-md point to one step on this scale.

Elevation shows how far off the page a component floats. Small, everyday pieces like an avatar or a badge sit close to the surface; big overlays like a modal float much higher, so they stay clearly above everything else. Drag the slider below and watch which step the card lands in.

Elevation: md

Card, dropdown menu, context menu, toast, and floating icon button.

PropertyValue
Stepmd
Semanticshadow-md
Offset8px
Blur16px

Elevation styles in use

Focus

A focus indicator shows which element is currently active. It's a core accessibility requirement: every interactive component must work with a keyboard alone, and the person using one needs to see where they are.

Two rules keep a focus ring usable. It must be at least 2px thick. And it must contrast at least 3:1 against whatever's behind it: if the ring colour would blend into the component, an offset gap keeps them apart. Both rules apply the same way in light and dark mode.

#0071F3border/focus
background-surface-default4.51:1Pass
background-surface-subtle4.03:1Pass
background-surface-strong3.85:1Pass

Checked live against WCAG 1.4.11's 3:1 minimum for non-text elements.

Focus styles

Three named styles set how the ring looks, chosen by what kind of component it's on. Default sits outside the shape. Inset changes the border itself, for tight spaces like inputs. Elevated adds a shadow, for surfaces that already float above the page.

Pressto focusTap to focus

LayerOffset x/yBlurSpreadColour token
Layer-10/0dp0dp1dpborder/light
Layer-20/0dp0dp3dpborder/focus

Grid

The grid only covers the content zone, the space between the sidebar and the contextual panel, not the whole page. It's built desktop-first: as the screen gets wider, its "Stretch" logic fills the extra space with more columns of data.

Four breakpoints set how many columns fit, from 12 down to 4 as the screen narrows. Drag the box below and watch which tier it lands in.

Laptop · 1024–1439px

Illustrative only — the drag range stands in for the real 320–1920px viewport, not true to scale.

The grid stays fluid between both side panels. Margins apply to the content zone, not the viewport edge.

PropertyValue
TierLaptop
Range1024–1439px
TypeStretch
Columns8
Gutter20px
Margin20px

A Message

Appsatile never launched. There was no public release, no self-serve signup, nothing anyone outside a few direct relationships could just go try. But it found real users anyway: a property management firm's team ran over 250 UK properties through it, and the accounts team at another startup managed their books on it for a while. Just over a hundred people, all told, actually opened it. Then the funding never came together, the team moved on, and the project was quietly sunset. For a while, that felt like the whole story.

But going back through this system now, I keep noticing our own brainstorming sessions turning up somewhere else: features we sketched and argued over, ideas for how AI could actually help someone get through their day instead of just summarising it for them, the whole shape of what we thought a business operating system should feel like. All of it got built anyway, by people who never saw our notes, and now sits inside ClickUp, Asana, Notion, Jira, and half the ERP and CRM tools teams run their work through today. That brings me a real, personal sense of victory. Not because we got there first, we didn't, but because it means we were on the right path. Our research was right.

A special thanks to Daria Zoria and Chinonso Ezeigbo, the two designers I designed Appsatile’s products and its design system with.

Appsatile was the work of a small group of genuinely talented designers and developers, and that's worth remembering on its own. The product is gone. The instinct it was built on clearly wasn't.