Skip to content
oakoliverPublic

About

CSS-like terminal styling for JavaScript. Zero dependencies. TypeScript port of charmbracelet/lipgloss.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

20 Commits

Folders and files

Repository files navigation

lipgloss

CSS-like terminal styling for JavaScript. Zero dependencies, multi-runtime (Node.js, Bun, Deno).

Terminal recording of bun examples/poster.ts: a block-letter LIPGLOSS banner with a pink-to-teal gradient, four border styles, a confirmation dialog placed on a patterned field, and two overlapping layers on a blend2D gradient

bun examples/poster.ts. Every picture in this README is recorded from the code in examples/ with @oakoliver/vhs; the tapes are in assets/tapes/.

Ported from charmbracelet/lipgloss (Go) to TypeScript.

Parity target: Lip Gloss v2.0.6. The Go-only synchronous terminal query functions BackgroundColor and HasDarkBackground, and the legacy Windows console helper EnableLegacyWindowsANSI, are excluded: JavaScript's supported Node, Bun, and Deno streams do not share Go's synchronous term.File query contract, and modern JavaScript runtimes enable Windows virtual-terminal processing themselves.

Read the article: Getting Started with Lip Gloss · Porting Lip Gloss and Glamour from Go

Install

npm install @oakoliver/lipgloss
# or
bun add @oakoliver/lipgloss
# or
deno add npm:@oakoliver/lipgloss

Quick Start

import { newStyle, normalBorder, roundedBorder, Center } from '@oakoliver/lipgloss';

// Create a style
const style = newStyle()
  .bold(true)
  .foreground('#FF6B6B')
  .background('#2D2D2D')
  .padding(1, 2)
  .border(roundedBorder())
  .borderForeground('#FF6B6B')
  .width(40)
  .align(Center);

console.log(style.render('Hello, Lipgloss!'));

Features

  • Fluent, immutable API — every setter returns a new Style (safe to share and compose)
  • CSS-like box model — padding, margin, border, width, height, alignment
  • 10 built-in border styles — normal, rounded, block, thick, double, hidden, ASCII, markdown, and more
  • Full ANSI color support — detected ASCII, ANSI, ANSI-256, and truecolor output profiles
  • Unicode cell correctness — ANSI-safe grapheme measurement for emoji, flags, combining text, and CJK
  • Text attributes — bold, italic, underline (5 styles), strikethrough, reverse, blink, faint
  • Layout utilities — joins, placement, patterned whitespace, wrapping, ranges, layers, and canvases
  • Color utilities — profile selection, CIELAB gradients, complement, brightness, and alpha
  • Zero dependencies — pure TypeScript using the runtime's built-in Unicode segmenter
  • TypeScript-first — full type declarations included

API

Style

import { newStyle, Style } from '@oakoliver/lipgloss';

const s = newStyle();

Every setter returns a new Style instance (immutable):

const base = newStyle().foreground('#7D56F4');
const title = base.bold(true).padding(0, 1);
const body = base.faint(true);

Text Attributes

bold, italic, underline, double, curly, dotted and dashed underlines, strikethrough, reverse and faint text rendered by lipgloss, each next to the method that produces it

s.bold(true)
s.italic(true)
s.underline(true)
s.underlineStyle('curly')  // 'none' | 'single' | 'double' | 'curly' | 'dotted' | 'dashed'
s.strikethrough(true)
s.reverse(true)
s.blink(true)
s.faint(true)

Colors

The 16 ANSI colors, the 256-color palette with its grayscale ramp, two true-color blend1D gradients, and darken, lighten and complementary swatches

// Hex (#RGB or #RRGGBB)
s.foreground('#ff0000')
s.background('#2D2D2D')
s.underlineColor('#00ff00')

// ANSI basic (0-15)
import { Red, BrightCyan } from '@oakoliver/lipgloss';
s.foreground(Red)

// ANSI 256 (16-255)
s.foreground(196)

// RGB object
s.foreground({ r: 255, g: 107, b: 107 })

Dimensions

s.width(40)       // fixed width (content wraps/pads to fit)
s.height(5)       // fixed height
s.maxWidth(80)    // truncate lines exceeding this
s.maxHeight(24)   // truncate output exceeding this many lines

Alignment

import { Left, Center, Right, Top, Bottom } from '@oakoliver/lipgloss';

s.align(Center)             // horizontal only
s.align(Center, Bottom)     // horizontal and vertical
s.alignHorizontal(Right)
s.alignVertical(Top)

Padding & Margin (CSS shorthand)

s.padding(1)          // all sides
s.padding(1, 2)       // vertical, horizontal
s.padding(1, 2, 3, 4) // top, right, bottom, left
s.paddingTop(1)
s.paddingLeft(2)

s.margin(1, 2)
s.marginBackground('#333')  // color the margin area

Borders

Every built-in border style: normal, rounded, thick, double, block, ascii, outer and inner half-block, hidden and markdown, plus a box with a different color per side

import {
  normalBorder, roundedBorder, blockBorder, thickBorder,
  doubleBorder, hiddenBorder, asciiBorder, markdownBorder,
  outerHalfBlockBorder, innerHalfBlockBorder,
} from '@oakoliver/lipgloss';

s.border(roundedBorder())                    // all sides
s.border(normalBorder(), true, false)        // top+bottom only
s.border(normalBorder(), true, true, true, true)  // explicit all

s.borderForeground('#FF6B6B')               // all sides same color
s.borderForeground('#f00', '#0f0', '#00f', '#ff0')  // CSS shorthand
s.borderBackground('#2D2D2D')

Other

s.inline(true)           // strip newlines
s.tabWidth(2)            // tab stop width (default 4, -1 to preserve tabs)
s.transform(s => s.toUpperCase())
s.hyperlink('https://example.com')
s.setString('prefix')    // prepend when rendering
s.colorWhitespace(true)  // apply bg color to padding spaces
s.underlineSpaces(true)
s.strikethroughSpaces(true)

Rendering

const output = style.render('Hello, World!');
console.log(output);

// render() joins multiple args with space
style.render('Hello,', 'World!');

// toString() renders the setString value
const s = newStyle().setString('Hello').bold(true);
console.log(`${s}`);  // bold "Hello"

Inherit

Copy style properties from another style (padding/margin excluded):

const parent = newStyle().bold(true).foreground('#FF6B6B');
const child = newStyle().italic(true).inherit(parent);
// child is now bold + italic + #FF6B6B foreground

Getters

Every property has a corresponding getter:

s.getBold()                // boolean
s.getForeground()          // Color | null
s.getWidth()               // number
s.getPadding()             // { top, right, bottom, left }
s.getHorizontalFrameSize() // margin + border + padding (horizontal)
s.getFrameSize()           // { x, y }

Layout

Terminal recording of bun examples/layout.ts, the port of Lip Gloss's own layout demo: tabs, a gradient title, a dialog with buttons, styled lists, a color grid, three wrapped text columns and a status bar

joinHorizontal with Top, Center and Bottom alignment, joinVertical with Left, Center and Right alignment, and place centering a box in a dotted area

import {
  joinHorizontal, joinVertical,
  place, placeHorizontal, placeVertical,
  Top, Bottom, Center, Left, Right,
} from '@oakoliver/lipgloss';

// Join blocks side by side (vertical alignment)
joinHorizontal(Top, blockA, blockB, blockC);
joinHorizontal(Center, blockA, blockB);

// Stack blocks vertically (horizontal alignment)
joinVertical(Left, blockA, blockB);
joinVertical(Center, blockA, blockB);

// Place content in an unstyled box
place(80, 24, Center, Center, 'Hello');
placeHorizontal(80, Center, 'Hello');
placeVertical(24, Center, 'Hello');

Color Utilities

import {
  parseColor, colorToRGB, isDarkColor,
  lightDark, complementary, darken, lighten, alpha,
} from '@oakoliver/lipgloss';

isDarkColor('#1a1a2e')     // true
isDarkColor('#ffffff')     // false

const pick = lightDark(isDarkColor('#1a1a2e'));
pick('#333', '#eee')       // returns '#eee' (dark background → use light color)

complementary('#ff0000')   // ~cyan
darken('#ff6b6b', 0.3)    // 30% darker
lighten('#333333', 0.2)   // 20% lighter
alpha('#ff0000', 0.5)     // { r: 128, g: 0, b: 0 }

ANSI Utilities

import { stripAnsi, stringWidth, truncate, styled, SGR } from '@oakoliver/lipgloss';

stripAnsi('\x1b[1mHello\x1b[0m')  // 'Hello'
stringWidth('\x1b[1mHello\x1b[0m') // 5 (ignores ANSI, handles CJK)
truncate(longString, 40)           // truncate to 40 visible chars
styled('Hi', { bold: true, fg: { type: 'rgb', value: 0, r: 255, g: 0, b: 0 } })

Attribution

This is a TypeScript port of charmbracelet/lipgloss, originally written in Go by Charmbracelet, Inc.. Licensed under MIT.

License

MIT

About

CSS-like terminal styling for JavaScript. Zero dependencies. TypeScript port of charmbracelet/lipgloss.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages