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'soptions), 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 withsetSpriteUrl(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.