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 version npm downloads

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)

  1. Drag both .ttf into 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.
  2. 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 to Resources (. = the Resources root):

    <key>ATSApplicationFontsPath</key>
    <string>.</string>
    
  3. Ask for it by PostScript namelaunchpad-icons-regular or launchpad-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.

Sizing

wrapper font-size
.li-*x

os-unit wrapping

change the --os-unit pixel value.

Animation

simulate loading progress:  
change the --color-progress hex value.

Styling

Font weight

Overrides

    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 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:

    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

    Two names for the same two shapes. The mask and box classes are declared in _variables.sass as .li-ellipse and .li-rounded, but _legacy.sass renames them to .li-rounded and .li-box whenever $legacy-support is true — 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-ellipse appears 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 .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.

    <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>
    

    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:

    5text 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 matching color-bg- or color-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 at currentColor — 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) in custom.sass so the examples above run; in a real app that set is Launchpad’s colors.css. Without either, use an inline background / color instead.

    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 .control on this page is a plain stand-in defined in custom.sass, for the same reason the color-* 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>