forge/README.md
Chris Wanstrath dafbce762b themes
2025-12-29 14:26:54 -08:00

3.7 KiB

⚒️ forge

╔═╝╔═║╔═║╔═╝╔═╝
╔═╝║ ║╔╔╝║ ║╔═╝
╝  ══╝╝ ╝══╝══╝

overview

Forge is a typed, local, variant-driven way to organize CSS and create self-contained TSX components out of discrete parts.

css problems

  • Styles are global and open - anything can override anything anywhere.
  • No IDE-friendly link between the class name in markup and its definition.
  • All techniques are patterns a human must know and follow, not APIs.
  • Errors happen silently.

forge solutions

  • All styles are local to your TSX components.
  • Styles defined using TS typing.
  • Component styles are made up of independently styled "Parts".
  • "Variants" replace inline styles with typed, declarative parameters.
  • Style composition is deterministic.
  • Themes are easy.
  • Errors and feedback are provided.

examples

styles

import { define } from "forge"

export const Button = define("button", {
  base: "button",

  padding: 20,
  background: "blue",
})

// Usage
<Button>Click me</Button>

variants

import { define } from "forge"

export const Button = define("button", {
  base: "button",

  padding: 20,
  background: "blue",

  variants: {
    status: {
      danger: { background: "red" },
      warning: { background: "yellow" },
    }
  },
})

// Usage
<Button>Click me</Button>
<Button status="danger">Click me carefully</Button>
<Button status="warning">Click me?</Button>

parts + render()

export const Profile = define("div", {
  padding: 50,
  background: "red",

  parts: {
    Header: { display: "flex" },
    Avatar: { base: "img", width: 50 },
    Bio: { color: "gray" },
  },

  variants: {
    size: {
      small: {
        parts: { Avatar: { width: 20 } },
      },
    },
  },

  render({ props, parts: { Root, Header, Avatar, Bio } }) {
    return (
      <Root>
        <Header>
          <Avatar src={props.pic} />
          <Bio>{props.bio}</Bio>
        </Header>
      </Root>
    )
  },
})

// Usage:
import { Profile } from "./whatever"

console.log(<Profile pic={user.pic} bio={user.bio} />)
console.log(<Profile size="small" pic={user.pic} bio={user.bio} />)

themes

built-in support for CSS variables with full type safety:

// themes.tsx - Define your themes
import { createThemes } from "forge"

export const theme = createThemes({
  dark: {
    bgColor: "#0a0a0a",
    fgColor: "#00ff00",
    sm: 12,
    lg: 24,
  },
  light: {
    bgColor: "#f5f5f0",
    fgColor: "#0a0a0a",
    sm: 12,
    lg: 24,
  },
})

// Use theme() in your components
import { define } from "forge"
import { theme } from "./themes"

const Button = define("Button", {
  padding: theme("spacing-sm"),
  background: theme("colors-bg"),
  color: theme("colors-fg"),
})

Theme switching is done via the data-theme attribute:

// Toggle between themes
document.body.setAttribute("data-theme", "dark")
document.body.setAttribute("data-theme", "light")

The theme() function is fully typed based on your theme keys, giving you autocomplete and type checking throughout your codebase.

scopes

Sometimes you want your parts named things like ButtonRow, ButtonCell, ButtonTable, etc, but all those Button's are repetitive:

const { define } = createScope("Button")

// css class becomes "Button"
const Button = define("Root", {
  // becomes "Button"
  // ...
})

// css class becomes "ButtonRow"
const ButtonRow = define("Row", {
  // ...
})

// css class becomes "ButtonContainer"
const ButtonContainer = define("Container", {
  // ...
})

see it

Check out the examples/ dir and view them at http://localhost:3300 by cloning this repo and running the local web server:

bun install
bun dev
open http://localhost:3300