Design system
Color mode

Drawstrings

A minimal, framework agnostic UI component library based on GitLab's Pajamas design

A minimal, framework-agnostic UI component library based on GitLab's Pajamas design system, built on Web Components (Lit). It also ships a Slippers (GitLab marketing) theme you can switch to at runtime.

Because the components are standard custom elements, they work anywhere HTML does — vanilla JS, Vue, React, Svelte, etc. — with no framework dependency.

Installation

Drawstrings is published to its GitLab project package registry. Point npm at that registry, then install.

Add to your project's .npmrc:

@gitlab-cxe:registry=https://gitlab.com/api/v4/projects/81384229/packages/npm/
npm install @gitlab-cxe/drawstrings

Every build is self-contained — Lit is bundled, so there is nothing else to install and dist/drawstrings.js loads as a plain module in a browser.

Quick start

Import the package to register every gl-* custom element, and import the stylesheet to load the design tokens and themes:

import '@gitlab-cxe/drawstrings' // registers the gl-* elements
import '@gitlab-cxe/drawstrings/styles' // → drawstrings.css (design tokens + Pajamas/Slippers/dark themes)
<gl-button variant="confirm">Save</gl-button>
<gl-badge variant="success">Passed</gl-badge>
<gl-alert variant="info" title="Heads up">Pipeline finished.</gl-alert>

Each component's own styling lives in Shadow DOM, so the elements render even without the stylesheet — but drawstrings/styles provides the canonical token values and is required for theming (the Slippers theme and any --gl-* overrides live there). The CSS is a separate artifact, so importing only the JS will not pull it in.

Browser / CDN (no build step)

The IIFE build exposes a global Drawstrings and self-registers the elements:

<link rel="stylesheet" href="drawstrings/dist/drawstrings.css" />
<script src="drawstrings/dist/drawstrings.min.js"></script>
<gl-button variant="confirm">Save</gl-button>
Framework usage

The elements are framework-agnostic. Two notes:

  • Vue 3 — tell the compiler that gl-* tags are custom elements so it doesn't try to resolve them as Vue components:

    // vite.config.js
    vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gl-') } } })
    
  • React — attributes (strings/booleans) work directly. For rich properties (arrays/objects like gl-select's options), set them via a ref, or use React 19's improved custom-element property handling.

Theming (Pajamas ↔ Slippers)

Pajamas is the default. Switching to Slippers is a pure token swap — no re-render, no per-component config. Set data-design-system on the document root, or use the helper:

import { setDesignSystem, getDesignSystem } from 'drawstrings'

setDesignSystem('slippers') // or 'pajamas' (default)
getDesignSystem() // → 'slippers'

Equivalently, in markup:

<html data-design-system="slippers">
  …
</html>

The attribute cascades to every component (custom properties inherit through Shadow DOM), so a single toggle re-skins the whole page.

Dark mode

Color mode is a separate axis from the design system — it composes with Pajamas, so you can run light or dark. Light is the default. Set data-color-mode on the document root, or use the helper:

import { setColorMode, getColorMode } from '@gitlab-cxe/drawstrings'

setColorMode('dark')  // 'light' (default) | 'dark' | 'auto'
getColorMode()        // → 'dark'

auto follows the operating system's prefers-color-scheme setting via a media query — no JavaScript listener required. Equivalently, in markup:

<html data-color-mode="dark">
  …
</html>

Dark mode is currently defined for the Pajamas design system; Slippers stays light for now. As with everything else, the styles ship in drawstrings.css, so import '…/styles' is required for dark mode to apply.

Customizing tokens

All styling flows through --gl-* CSS custom properties. Override them at :root (or any scope) to tune the look without forking components:

:root {
  /* Brand the primary action */
  --gl-button-confirm-bg: #7759c2;
  /* Or remap the palette wholesale */
  --gl-color-blue-500: #1a73e8;
}

Component-specific tokens (e.g. --gl-button-confirm-bg, --gl-link-color) fall back to the semantic color scale (--gl-color-blue-500, …), which falls back to a literal default — so you can override at whichever level you need.

Server-side rendering (declarative shadow DOM)

Component styles live inside each element's shadow root, which does not exist until the element upgrades. On a server-rendered page the markup paints first and stays unstyled until then. The opt-in ./ssr entry emits <template shadowrootmode="open"> into the HTML so the real component CSS is in the document at first paint:

import { injectDeclarativeShadowRoots } from '@gitlab-cxe/drawstrings/ssr'

// run it last, on your framework's rendered HTML
const html = await injectDeclarativeShadowRoots(await renderToString(tree))

Requires the optional peer @lit-labs/ssr, which is not installed for you:

npm i @lit-labs/ssr   # a runtime dependency, not a dev one, if you render per request

If it is missing, or anything throws, the HTML is returned unchanged and a [drawstrings/ssr] Lit SSR unavailable warning goes to the server console — the client-side upgrade path is never made worse. A missing install therefore shows up as unstyled first paint rather than a failed render, so check the warning in the environment that actually renders.

If your server bundle has no node_modules at runtime (a Cloudflare Worker, a Lambda), every dependency the SSR pass touches has to be bundled into it — with Vite:

// vite.config.ts
ssr: { noExternal: true }

If bundling everything is too broad — a native module, or anything that resolves require/__dirname at runtime, has to stay external — list the SSR chain instead, transitive deps included:

ssr: {
  noExternal: [
    '@gitlab-cxe/drawstrings',
    '@lit-labs/ssr',
    '@lit-labs/ssr-dom-shim',
    '@lit-labs/ssr-client',
    'lit',
    'lit-html',
    'lit-element',
    '@lit/reactive-element',
    'parse5',
    '@parse5/tools',
    'entities',
  ],
}

The last three matter: naming only the Lit packages leaves parse5 external, and adding parse5 alone then leaves entities. Either one missing fails the dynamic import — which this entry catches, so you get an unstyled page rather than a build or boot error.

Nothing extra is needed on the client: importing @gitlab-cxe/drawstrings applies Lit's hydrate support to the Lit copy it bundles, so an element upgrading over a server-rendered shadow root hydrates it rather than rendering a second copy beside it.

Two notes for React consumers. Run the transform after renderToString / getDataFromTree rather than rendering a <template> in JSX: the parser consumes the template into a shadow root before hydration, so React would find a node it cannot see. And keep custom-element registration deferred until after hydration — Lit reflects default properties onto the host on upgrade, which React reports as "Extra attributes from the server".

Brands

A brand is a consumer's own skin: --gl-* overrides layered over whichever design system is active. Brand is a third axis, so it composes with the design system and the color mode rather than replacing either.

import '@gitlab-cxe/drawstrings/styles'
import '@gitlab-cxe/drawstrings/brands/gitlab-university' // must come second
import { setBrand } from '@gitlab-cxe/drawstrings'

setBrand('gitlab-university') // or 'none' (default)
<html data-design-system="slippers" data-brand="gitlab-university">

Each brand is a separate entry, so consumers that do not use one download nothing. Import it after ./styles: brand selectors have the same specificity as the theme block, so source order decides.

Icons

<gl-icon> renders from a built-in registry of Pajamas icons:

<gl-icon name="check" size="16" label="Done"></gl-icon>
  • Custom icons / sprites: add to the registry with registerIcon(name, svg, viewBox?), or point at a GitLab SVG sprite with setSpriteUrl(url).

  • Slippers marketing icons are large and shipped as an opt-in entry that registers them under slp-* names:

    import '@gitlab-cxe/drawstrings/slippers-icons'
    
    <gl-icon name="slp-announce-release" size="32"></gl-icon>
    
Development
npm run dev        # Vite dev server + demo page (index.html)
npm run build      # build dist/ (ESM, CJS, IIFE, CSS, types)
npm run typecheck  # tsc --noEmit
npm run test:e2e   # Playwright smoke tests against the demo page

The demo page includes a Pajamas/Slippers switcher and a Light/Dark/Auto color-mode switcher to preview the themes.

Component demos

Button

Default Confirm Danger Dashed Disabled Link

Badge

Neutral Muted Info Success Warning Danger 42

Avatar

Alert

Something you should know. Your changes were saved. Something went wrong.
Retry

Label

Select

Form Group

Input

Checkbox

Icon

Accordion

GitLab is a complete DevSecOps platform delivered as a single application. Sign up at gitlab.com, create a project, and push your first commit. GitLab Community Edition is open source under the MIT license. This content is inaccessible.

Avatar Group

Button Group

Day Week Month Merge

Card

This project contains the source code for the GitLab web interface.

View details
New Pipeline results

3 of 4 jobs passed. See the full report for details.

Banner

Unlock advanced security scanning, compliance pipelines, and more.
Upgrade now Learn more
AI-powered features are now available across your organization.
Try GitLab Duo

Link

Default link Subtle link Danger link External link

Segmented Control

Token

Truncate

Skeleton

Stepper

← Prev Next →

Drawer

Open right drawer Open left drawer
Cancel Save

Left-side navigation drawer.

Close

Popover

Show info

3 of 4 jobs passed. The test:unit job failed.

No title This popover has no title — just body content. Top Appears above the trigger.

Textarea

Pagination

Toast

Info toast Success toast Warning toast Danger toast Persistent toast

Toggle

Radio

Spinner

Progress

Breadcrumb

GitLab gitlab-org gitlab Issues

Tooltip

Copy Open Delete

Tabs

This is the Overview tab panel.

Recent activity will appear here.

Releases content.

Configure your project settings.

Modal

Open small Open medium Open large

This pipeline and all of its jobs will be permanently deleted. This action cannot be undone.

Cancel Delete pipeline
Cancel Save changes

Large modal content goes here. Useful for forms with many fields or wider content like tables.

Cancel Apply

Dropdown

Edit Duplicate Delete Approve MR Merge Rebase and merge Never shown Profile Settings Sign out
View profile Settings Sign out

Empty State

New merge request Learn more

Search Input

Character Count

Table

Combobox

Date Picker

Collapse

Toggle details
Collapse is a low-level show/hide primitive. Unlike accordion it has no built-in chrome — supply your own trigger and content.
Expanded by default
This panel starts open because the visible attribute is set.

Tree

Path

Attribute List

Active Private April 16, 2026 Mike Leopard 12345678 main

Animated Number

Pipelines run
Coverage
Randomize

Broadcast Message

GitLab will undergo scheduled maintenance on Saturday at 02:00 UTC. Learn more A new version of GitLab is available for your self-managed instance. Your subscription has been renewed successfully.

Input Group

https:// .git

Typography

Display Heading 1 Heading 3 Heading 5 Body 1 — lead paragraph text. Body 2 bold All caps label

Grid

span 6
span 6
12 / md 4
12 / md 4
12 / md 4

Section & Side Nav

In a section band The section paints a full-width background; the grid lays out the nav and content.