Launchpad Icons
The icon set for the dbo.io Framework. Every icon is a single class with the li- prefix — drop it on an <i> (or any element) and it renders from a glyph font.
<i class="li-dbo"></i>
Install
Two font weights — regular (400) and lighter (100) — sharing one character map. Each ships twice: WOFF2 for the web and TTF for native targets (Native). You only ever link one stylesheet. The package ships compiled CSS + fonts only — no source.
npm — install the scoped package:
npm install @dboio/launchpad-icons
import '@dboio/launchpad-icons' // launchpad-icons.css
import '@dboio/launchpad-icons/min' // minified
CDN (jsDelivr) — no build step, pin a version (or @latest):
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@dboio/launchpad-icons@latest/dist/assets/css/launchpad-icons.min.css">
Vendor — or copy dist/assets/ into your project and link the stylesheet, keeping the two .woff2 files at the ../fonts path the CSS expects:
<link rel="stylesheet" href="/assets/css/launchpad-icons.min.css">
Then use any class:
<i class="li-dbo"></i>
Native (Xcode, Android)
The web loads WOFF2. Apple platforms cannot read it at all — CoreText accepts only ttf/otf — and Android’s Font() loader wants a ttf too. So every weight also ships as a .ttf, from the same build as the web font: same 366 glyphs, same codepoints, same character map.
node_modules/@dboio/launchpad-icons/dist/assets/fonts/launchpad-icons-regular.ttf
node_modules/@dboio/launchpad-icons/dist/assets/fonts/launchpad-icons-lighter.ttf
Those are the on-disk paths, which is what you want when you are dragging a file into Xcode. Anything resolver-driven — a Gradle or Xcode copy step, a Metro/Expo require(), a bundler — should ask the package instead, so the path survives a hoisted or pnpm-linked node_modules:
require.resolve('@dboio/launchpad-icons/fonts/launchpad-icons-regular.ttf')
import.meta.resolve('@dboio/launchpad-icons/fonts/launchpad-icons-lighter.ttf')
Every file in dist/assets/fonts is reachable that way, .woff2 as well as .ttf.
Not using npm? Take them straight from the CDN:
https://cdn.jsdelivr.net/npm/@dboio/launchpad-icons@latest/dist/assets/fonts/launchpad-icons-regular.ttf
Xcode (iOS and macOS)
- Drag both
.ttfinto the project. Tick Copy items if needed, and — the step that actually matters — tick your app target. Without target membership the file is in the project navigator but not in the bundle, which is the usual reason a font silently refuses to load. -
Declare it in
Info.plist.iOS — Fonts provided by application (
UIAppFonts), filenames relative to the bundle:<key>UIAppFonts</key> <array> <string>launchpad-icons-regular.ttf</string> <string>launchpad-icons-lighter.ttf</string> </array>macOS —
ATSApplicationFontsPath, a path relative toResources(.= the Resources root):<key>ATSApplicationFontsPath</key> <string>.</string> - Ask for it by PostScript name —
launchpad-icons-regularorlaunchpad-icons-lighter. Not the file name, and not a family plus a weight modifier.
// li-dbo — see "Finding a codepoint" below
Text(String(UnicodeScalar(0xE87C)!))
.font(.custom("launchpad-icons-regular", size: 17, relativeTo: .body))
relativeTo: opts the glyph into Dynamic Type, so it scales with the text beside it. UIKit and AppKit take the same name: UIFont(name: "launchpad-icons-regular", size: 17).
The weight is the family, not a modifier. Both weights report subfamily
Regular, so.fontWeight(.light)will not get you the lighter face — name it directly.
Loading from somewhere other than the app bundle — a Swift package resource, a downloaded file, a playground — register it yourself first:
var error: Unmanaged<CFError>?
CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error)
Finding a codepoint
On the web the stylesheet carries the character map, so you never see a codepoint. Native has no stylesheet, so you need the scalar itself. It ships inside the package, next to the fonts:
import codepoints from '@dboio/launchpad-icons/codepoints' with { type: 'json' }
codepoints.icons.dbo // 59516 → 0xE87C
node_modules/@dboio/launchpad-icons/dist/assets/codepoints.json
https://cdn.jsdelivr.net/npm/@dboio/launchpad-icons@latest/dist/assets/codepoints.json
{
"version": "5.1.0",
"fonts": { "regular": "launchpad-icons-regular", "lighter": "launchpad-icons-lighter" },
"metrics": { "unitsPerEm": 1000, "ascent": 850, "descent": 150 },
"icons": { "dbo": 59516, "alert": 59481, "…": 0 }
}
All 401 classes, aliases included — li-instance and li-drive resolve to the same glyph, so they are one scalar to a native caller. Codes are decimal: 59516 is 0xE87C.
Take it from the package, not from this website. These docs are built from
main, so the gallery’s own JSON always describes the latest source. The packaged map travels with the version you installed — whatever you pin, the map beside the font describes that font.
Rather than scatter magic numbers, generate a type once and use names:
enum LaunchpadIcon: UInt32 {
case dbo = 0xE87C
case alert = 0xE859
// …generated from config.json
var char: String { String(UnicodeScalar(rawValue)!) }
}
Text(LaunchpadIcon.dbo.char)
.font(.custom("launchpad-icons-regular", size: 17, relativeTo: .body))
Metrics
unitsPerEm 1000, ascent 850, descent 150 — glyphs sit on the text baseline, so they line up with adjacent text without nudging. At 17 pt that is 14.5 up and 2.5 down.
Don’t mix these with SF Symbols in one row. The two families centre on different vertical conventions — launchpad on the text cap band, SF Symbols about 0.15 em higher. The glyph metrics here are correct as they are; nudge the SF Symbol, not the icon.
Android
Android resource names must be lowercase with underscores, so rename on the way in:
app/src/main/res/font/launchpad_icons_regular.ttf
val icons = FontFamily(Font(R.font.launchpad_icons_regular))
Text(text = "\uE87C", fontFamily = icons)
Icons
Every class in the font. Search by name or keyword, then click any icon to copy its class — or Alt-click to copy the full <i> element.
Builder
Toggle conditional classes, search for an icon, and copy the generated snippet. Live preview updates as you change options.
Code snippet
Click an icon above to generate its snippet.
Sizing
By default an icon inherits its size from the surrounding font-size. Override it with any of these:
.li-sm— renders smaller than the inherited size. In combination with a styling class (.li-disc,.li-circle, …) the inner graphic is scaled back further inside its shape..li-lg— renders 1.333× the inherited size. (Note: this is not the same as.li-1x— it sits between1×and2×.).li-1x….li-10x— multiply the icon’s natural size by N..li-2xis twice the surrounding size,.li-3xthree times, up to.li-10xat ten times. The scale tracks the text the icon sits in (and the.os-unitvalue when one is set).
.li-sm
base
.li-lg
.li-2x
.li-3x
<i class="li-star li-sm"></i>
<i class="li-star"></i>
<i class="li-star li-lg"></i>
<i class="li-star li-2x"></i>
<i class="li-star li-3x"></i>
os-unit
.os-unit pins an icon to a fixed pixel box instead of letting it scale with the surrounding font-size. This is how LaunchpadOS keeps icon controls uniform across a UI regardless of nearby text — and it’s the natural size for disc/rounded backings, badges, and spinners, which all scale within the unit.
The box comes from the --os-unit CSS variable (default 3rem, i.e. 48px). Apply it two ways:
- on the icon —
<i class="li-app li-disc os-unit"></i> - on a wrapper — every
li-*inside an.os-unitelement inherits the unit.
Resize by setting --os-unit inline (or on a container):
32px
default · 48px
72px
<i class="li-app li-disc os-unit"></i> <!-- 48px default -->
<i class="li-app li-disc os-unit" style="--os-unit: 72px"></i> <!-- resized -->
<!-- or size a whole group from a wrapper -->
<div class="os-unit" style="--os-unit: 32px">
<i class="li-app li-disc"></i>
<i class="li-bell li-disc"></i>
</div>
The Builder lets you change --os-unit live and see icons respond.
Styling
.li-circle— outlines the icon with a circle and scales the graphic down to 60%..li-disc— underlays a solid disc, scales the graphic to 60%, and inverts the icon color on top of the disc color..li-rounded— clips (masks) the icon in a circular shape. It auto-inverts the glyph against the backing, but setting your ownbackgroundturns that off (the rule is guarded by:not([style*=background]), so your value wins outright) — so pair an inlinebackgroundwith an explicitcolor. In a source build with$legacy-support: falsethis class is named.li-ellipse— see the note below..li-box— underlays a solid rounded box, scales the graphic to 60%, and inverts the icon color on top. Named.li-roundedin a$legacy-support: falsebuild..li-border— outlines the icon with a border. Combinable with any other style..li-flip-horizontal,.li-flip-vertical— flip the icon on either axis..li-rotate-90,.li-rotate-180,.li-rotate-270— rotate the icon. Has no effect while.li-spinor.li-spin-counteris applied.
Two names for the same two shapes. The mask and box classes are declared in
_variables.sassas.li-ellipseand.li-rounded, but_legacy.sassrenames them to.li-roundedand.li-boxwhenever$legacy-supportistrue— which is the default, and what the published package and the CDN build ship. So the shipped names are.li-rounded(mask) and.li-box(box);.li-ellipseappears only if you compile the SASS yourself with$legacy-support: false. Everything on this page uses the shipped names.
.li-circle
.li-disc
.li-rounded
.li-box
.li-border
.li-flip-horizontal
.li-rotate-90
<i class="li-star li-circle"></i>
<i class="li-star li-disc"></i>
<i class="li-star li-rounded" style="background: cornflowerblue; color: #fff"></i> <!-- .li-ellipse in a non-legacy build; own background ⇒ set color too -->
<i class="li-star li-box"></i> <!-- .li-rounded in a non-legacy build -->
<i class="li-star li-border"></i>
<i class="li-arrow-e li-flip-horizontal"></i>
<i class="li-arrow-e li-rotate-90"></i>
Font weight
Two faces are bound: regular (the default) and lighter (100). Trigger the lighter face with font-weight: lighter, or the class .li-light / .li-lighter.
The class hooks for a bold face (.li-bold, .li-bolder, nesting inside <strong> / <b>) exist in the CSS, but no bold font file is shipped yet — they’re wired ahead of a future bold weight and currently fall back to regular.
regular
.li-light
<i class="li-media"></i>
<i class="li-media li-light"></i>
Animation
.li-spin— spins the icon graphic clockwise..li-spin-counter— spins it counter-clockwise..loading— slightly modifies and pulsates the two dots of.li-optionsand.li-options-horizontal.
.li-spin
.li-spin-counter
.loading
<i class="li-refresh li-spin"></i>
<i class="li-refresh li-spin-counter"></i>
<i class="li-options-horizontal loading"></i>
The spinner sits on the shape’s own circle
There is one circle per shaped icon. .li-circle outlines it, .li-disc fills it, and while .loading the spinner ring is that outline — same centreline, same thickness. Put a loading icon next to its idle twin at any size and the ring should land exactly where the outline was.
.li-circle
+ .loading
.li-disc
+ .loading
The same four at twice the size — the ring is derived from the shape’s box, so it holds wherever --os-unit lands:
.li-circle
+ .loading
.li-disc
+ .loading
<i class="li-star li-disc os-unit loading">
<div class="li-spinner">
<svg class="circular" viewBox="25 25 50 50"><circle cx="50" cy="50" r="16"></circle></svg>
</div>
</i>
A host can resize the ring with --icon-spinner-scale-override — but note it replaces the derived geometry rather than multiplying it, so setting it to 1 opts out of the coincidence rather than confirming it.
Morph
live — cycles every state
Six icons can animate dynamically into each other. Apply li-morph plus the specific morph class. Combining a morph with li-circle or li-disc is not supported — use li-rounded or li-box when you need a disc-type background.
.li-morph-plus.li-morph-minus.li-morph-edit.li-morph-view.li-morph-close.li-morph-equal
<i class="os-unit li-rounded color-bg-canvas color-fg-widget li-morph li-morph-close"></i>
Animate by swapping the class with JavaScript:
let animateIcon = function(icon) {
icon.classList.replace('li-morph-close', 'li-morph-view');
icon.classList.replace('color-fg-widget', 'color-fg-view');
setTimeout(() => {
icon.classList.replace('li-morph-view', 'li-morph-minus');
icon.classList.replace('color-fg-view', 'color-fg-delete');
setTimeout(() => {
icon.classList.replace('li-morph-minus', 'li-morph-edit');
icon.classList.replace('color-fg-delete', 'color-fg-edit');
setTimeout(() => {
icon.classList.replace('li-morph-edit', 'li-morph-plus');
icon.classList.replace('color-fg-edit', 'color-fg-add');
setTimeout(() => {
icon.classList.replace('li-morph-plus', 'li-morph-equal');
icon.classList.replace('color-fg-add', 'color-fg-view');
setTimeout(() => {
icon.classList.replace('li-morph-equal', 'li-morph-close');
icon.classList.replace('color-fg-view', 'color-fg-widget');
}, 1500)
}, 1500)
}, 1500)
}, 1500)
}, 1500)
}
Overrides
Overrides swap the icon graphic when a condition is met — useful for on / off visualization. The samples below are live — click to toggle.
.li-off
.li-checkbox
.li-radiobutton
.li-lock
<label><input type="checkbox" hidden><i class="li-off"></i></label>
<label><input type="checkbox" hidden><i class="li-checkbox"></i></label>
<label><input type="radio" hidden><i class="li-radiobutton"></i></label>
<label><input type="checkbox" hidden><i class="li-lock"></i></label>
.fullscreenand:fullscreen— swap.li-expandfor.li-contract.-
li-checkbox,.li-radiobutton,.li-on,.li-off,.li-off-alt,.li-lock,.li-unlock— react to a precedingcheckbox/radioinput inside a<label>, or a checked<input>.<label><input type="checkbox" hidden><i class="li-on"></i></label> <label><input type="checkbox" hidden><i class="li-checkbox"></i></label> <label><input type="radio" hidden><i class="li-radiobutton"></i></label> <label><input type="checkbox" hidden><i class="li-lock"></i></label> -
When
$selected-supportistrue(or the class.li-selected-supportis on the icon), the classselectedplus:active/:focuson the icon or a parent toggles the solid rendition, and the selected/checked state ofli-off,li-checkbox, andli-radiobutton. The default isfalse, so only.selecteddirectly on the icon switches it to solid;.selectedon a parent and:active/:focusare ignored. Selected states for.li-radiobutton,.li-checkbox, and.li-offalways respond to a parent.selected.<!-- all render the solid state of li-alert --> <span class="selected"><i class="li-alert"></i></span> <!-- only when $selected-support: true --> <i class="li-alert selected"></i> <i class="li-alert-solid"></i> <!-- selected state for controls --> <i class="li-checkbox selected"></i> <!-- renders li-checkbox-checked --> <i class="li-radiobutton selected"></i> <!-- renders li-radiobutton-checked --> <i class="li-off selected"></i> <!-- renders li-on -->
Badges
Overlay a badge — an icon or a bit of text — that floats above the icon. The default position is top-right; move it with tl, bl, or br for the other corners. Add the badge as inner markup:
text badge
icon badge · tl
<i class="li-app li-disc color-bg-app">
<div class="li-badge tl"><span class="li-close li-disc"></span></div>
</i>
<a class="li-app li-disc color-bg-app">
<span class="li-badge color-bg-delete color-fg-white">
<small>55</small>
</span>
</a>
color-bg-*/color-fg-*come from Operator, not from this package. launchpad-icons only provides the hook: it stands its own defaults down whenever a class matchingcolor-bg-orcolor-fg-is present, expecting yours to supply the color. That means dropping one of these classes into a project that doesn’t define it leaves the glyph atcurrentColor— the same value as the disc beneath it — and the icon goes black on black. These docs define a small demo set (canvas,widget,app,delete,white) incustom.sassso the examples above run; in a real app that set is Launchpad’scolors.css. Without either, use an inlinebackground/colorinstead.
Spinners
spinner
progress · 50%
Add a child element for loading indication. The default spinner simply rotates; you can also drive a progress indicator from a percentage. The class .loading on the outer icon reveals the spinner (hidden otherwise). Set its color with the --color-progress CSS variable — on this page it defaults to #2ad8fa, and the Builder’s color picker drives every spinner shown here. Adding li-lighter to the li-spinner thins the spinner stroke only — not the parent icon.
<!-- default spinner that just rotates -->
<i class="li-check li-disc loading" style="--color-progress: #2ad8fa">
<div class="li-spinner li-lighter"><svg class="circular" viewBox="25 25 50 50"><circle cx="50" cy="50" r="16"></circle></svg></div>
</i>
<!-- spinner with progress -->
<i class="li-check li-disc li-lighter loading" data-progress="50%">
<div class="li-spinner"><svg class="circular" viewBox="25 25 50 50"><circle cx="50" cy="50" r="16"></circle></svg></div>
</i>
Cross-engine check
The ring must sit concentric with the icon’s round backing, at the same size, on every engine. Its SVG carries no intrinsic width or height, so it is pinned to the icon’s os-unit box in CSS rather than left to per-engine intrinsic sizing — otherwise Blink and WebKit disagree and the ring renders at two different sizes. Open this row side by side in Chrome and Safari: every ring should hug its backing identically in both.
li-disc · lighter
li-circle
li-rounded
li-box
32px
48px
72px
<!-- the os-unit form — the spinner scales within the unit -->
<i class="li-execute li-disc os-unit loading">
<div class="li-spinner li-lighter"><svg class="circular" viewBox="25 25 50 50"><circle cx="50" cy="50" r="16"></circle></svg></div>
</i>
Inside a control
.control, .controls and .os-form-submit are ancestor hooks. When one of them sits above a .loading icon that also carries a color-fg-* class, launchpad-icons washes the icon with rgba(255,255,255,.2) and pins its glyph back to currentColor — so the control reads as busy as a whole, rather than as a button that has merely sprouted a spinner. Nothing needs to be added to the icon itself; the ancestor is the whole trigger.
idle
loading
loading · 50%
<label class="control">
<i class="li-execute li-rounded os-unit color-bg-canvas color-fg-widget loading">
<div class="li-spinner"><svg class="circular" viewBox="25 25 50 50"><circle cx="50" cy="50" r="16"></circle></svg></div>
</i> Running
</label>
Pick the icon’s backing so it contrasts with the control’s own text color, not with the color-fg-* you set: while loading, the glyph is pinned to currentColor, so a color-fg-* chosen against a dark backing goes invisible the moment the control starts working. The pairing above (color-bg-canvas + color-fg-widget) holds in both themes because the two tokens flip together.
The ring is the stroke of that circle, and it is never filled. A fill on the same circle is not a backing behind the ring — it paints a solid disc out to the ring’s own radius and leaves only the stroke width showing, which is what #11 was. The round backing you see behind the ring belongs to the shape class, drawn on the icon’s :after and sitting below the spinner.
The
.controlon this page is a plain stand-in defined incustom.sass, for the same reason thecolor-*classes are — Operator owns the real one. Any ancestor with the class name works; the package only ever looks at the name.
Stacking & sandwiching
Sandwich multiple icons by nesting them. Use nestable elements — you can’t put an <i> inside an <i>, so reach for <span>.
stacked
<span class="li-circle-thick color-fg-info">
<span class="li-users color-fg-delete"></span>
</span>