No description
  • TypeScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Errilaz 46003af46f
Some checks failed
Checks / checks (push) Failing after 1s
Mirror to GitHub / mirror (push) Failing after 1s
Checks / release (push) Has been skipped
docs: syntax-extensions
2026-09-04 01:38:44 +02:00
.agents chore: config sync 2026-07-25 12:40:46 +02:00
.forgejo/workflows feat: release packages 2026-05-07 15:33:07 +02:00
.husky feat: first 2026-05-07 14:51:26 +02:00
.internal docs: syntax-extensions 2026-09-04 01:38:44 +02:00
docs/adr docs: syntax-extensions 2026-09-04 01:38:44 +02:00
examples/todomvc fix: various issues 2026-05-12 01:43:32 +02:00
openspec docs: syntax-extensions 2026-09-04 01:38:44 +02:00
packages feat: export more bundler plugins 2026-07-25 10:45:30 +02:00
.gitignore chore: sync config 2026-07-25 12:15:48 +02:00
.mirrorignore docs: syntax-extensions 2026-09-04 01:38:44 +02:00
AGENTS.md chore: regime sync 2026-05-19 00:06:23 +02:00
bun.lock feat(typed-html-attributes): add attribute discovery 2026-05-20 02:09:30 +02:00
CLAUDE.md feat(vdom): raw nodes in rules 2026-05-13 13:38:03 +02:00
commitlint.config.ts feat: first 2026-05-07 14:51:26 +02:00
CONTEXT.md docs: syntax-extensions 2026-09-04 01:38:44 +02:00
hypeup.code-workspace feat: export more bundler plugins 2026-07-25 10:45:30 +02:00
LICENSE feat: first 2026-05-07 14:51:26 +02:00
opencode.json chore: sync config 2026-07-25 12:15:48 +02:00
oxfmt.config.ts chore: config updates 2026-05-16 01:17:52 +02:00
oxlint.config.ts chore: config updates 2026-05-16 01:17:52 +02:00
package.json feat: export more bundler plugins 2026-07-25 10:45:30 +02:00
README.md docs: update readme 2026-07-25 12:26:04 +02:00
regime.config.json chore: config updates 2026-05-16 01:17:52 +02:00
regime.internal.json chore: regime sync 2026-05-19 00:06:23 +02:00
release.config.cjs feat: release packages 2026-05-07 15:33:07 +02:00

hypeup

Pure TypeScript UI Framework.

hypeup is a beyond-hyperscript style UI framework where all HTML elements and CSS properties are available globally — no imports needed. It supports server-side rendering, client-side mounting, and static site generation.

  • Readable markup in TypeScript, no TSX necessary
  • Static site generation via hypeup generate
  • Experimental fast client-side framework inspired by Mithril
  • Build plugins for Vite, esbuild, Rollup, Rolldown, Farm, Bun, webpack, and Rspack

Markup

HTML elements are available as global functions and render to HTML.

div(
  span("Password: "),
  input({ type: "password" }),
)

Strings, numbers, arrays, etc. are supported as children. null, undefined, and false render as empty. Attributes are defined with plain {} objects and are strongly typed.

div({ id: "profile", class: "card" })
input({ type: "password", placeholder: "Password", readonly: true, maxlength: 40 })
label({ for: "email" }, "Email")

Attribute names use HTML spelling, not DOM property aliases. For example, use readonly, maxlength, and for instead of readOnly, maxLength, and htmlFor.

Common attribute values are typed for autocomplete and validation, while open-ended values such as custom link targets are still allowed.

input({ type: "email" })
a({ target: "preview-window" })

Boolean attributes render naturally: true includes the attribute and false omits it.

input({ disabled: true }) // <input disabled>
input({ disabled: false }) // <input>

For custom attributes or custom tags, use attr() and elem():

div(attr("data-state", state))
elem("my-widget", attr("custom-attr", "value"))

You can pass multiple attribute objects wherever it reads best.

a.someClass(
  "My Link",
  { href: "/my_link" },
  { class: "another-class" },
  className("a-third-class"),
)

Use raw() for content that should not be escaped:

raw("<span>hello!</span>")

raw() can also be used inside rules and at-rules.

Element Class Shorthand

You can apply classes directly to element functions:

div(
  div.redBold("this is bold and red!"),
  div.redBold.alsoItalic("this has two classes!"),
)

Class names are automatically converted to kebab-case.

Styles

All standard and known vendor-specific CSS properties are global functions:

color("#ff0000"),
border("solid 1px red"),
webkitBorderImageWidth("4px"),

Standard values are also available as properties on these functions:

color.red,
borderStyle.dashed,

Inline Styles

You can add CSS properties directly to elements:

div(
  color.red,
  fontWeight.bold,
  "this is bold and red!",
)

Rules

Use rule() for CSS rules and prop() for custom properties.

style(
  rule(".red-bold",
    color.red,
    fontWeight.bold,
    prop("--some-custom", "value"),
  ),
)

Rule Class Shorthand

Class names may be used as selectors via dot syntax (converted to kebab-case):

rule.container(
  width("1200px"),
)

Element functions may be used as selectors:

rule(textarea,
  borderColor.black,
)

Nested Rules

Rules may be nested:

rule(".danger",
  color.red,
  rule(".icon",
    float.right,
  ),
)

Use & to combine a nested selector with its parent:

rule(".danger",
  color.red,
  rule("&.large",
    fontSize("40px"),
  ),
)

Nested selectors with pseudo-classes:

rule(a,
  color.red,
  textDecorationLine.none,
  rule(":hover",
    textDecorationLine.underline,
  ),
)

Multiple selectors in a rule generate the necessary CSS:

rule("input, textarea",
  border("solid 1px gray"),
  rule(":hover, :focus",
    borderColor.black,
  ),
)

Native CSS Nesting

Prefix a nested selector with / to keep it nested in the output:

rule(".parent",
  color.red,
  rule("/.child",
    color.blue,
  ),
)

The / is removed when rendering. This also works with selectors such as /&:hover, /.className, and / > li.

At-rules

Media queries and other at-rules are supported with the $ prefix:

$media("(prefers-color-scheme: dark)",
  rule(":root",
    prop("--fg", "white"),
    prop("--bg", "black"),
  ),
)
$layer(
  rule("p",
    color.red,
  ),
)

Components

Components are plain functions that return markup:

function Greeting(name: string) {
  return div(
    h1("Hello, ", name, "!"),
    p("Welcome to the site."),
  )
}

Used as regular function calls:

div(
  Greeting("world"),
  Greeting("hypeup"),
)

Components are just functions. They can accept any arguments and return any valid content. Capitalize component names so build tools can optimize them.

Client Runtime

The experimental client runtime provides mounting and event handling for interactive applications.

Mounting

import "@hypeup/lexicon"
import { mount } from "@hypeup/client"

function App() {
  return div(
    h1("Hello, world!"),
  )
}

mount(document.getElementById("app")!, () => App())

Events

Use on to bind event handlers:

button(
  "Click me",
  on("click", () => {
    console.log("clicked!")
  }),
)

Redraw

Call redraw() after mutating state to update the page.

Refs

Use ref to get a reference to a DOM element:

const myInput = ref<HTMLInputElement>()

input(myInput, { type: "text" })

// later...
myInput.current?.focus()

Lists

Use each to render lists with efficient reconciliation:

each(items, (item) => li(item.name))

With a key function for stable identity:

each(items, (item) => item.id, (item) => li(item.name))

Static Site Generation

The hypeup CLI generates static output from files using a double-extension convention. The first extension is the target format and the second is the source language:

  • .html.ts / .html.js -- generates an HTML file
  • .css.ts / .css.js -- generates a CSS file
  • .md.ts / .md.js -- generates a Markdown file

If the build tool supports other languages, those work too (e.g. .html.civet).

hypeup generate --dir src --out dist

Configuration File

Project defaults can live in hypeup.config.ts at the project root:

import { defineConfig } from "hypeup"

export default defineConfig({
  dir: "src",
  out: "dist",
  clean: true,
  port: 5173,
  vite: {
    resolve: {
      alias: {
        "@": new URL("./src", import.meta.url).pathname,
      },
    },
  },
})

Config files can be TypeScript, JavaScript, ESM, or JSON.

CLI flags override config file values:

hypeup generate --out build

Use the vite key to customize Vite during generation and watch mode.

File Convention

Each file's default export should be a function returning content. For HTML files, return elements:

// index.html.ts
import "@hypeup/lexicon"

export default function Index() {
  return [
    doctype.html5,
    html(
      head(title("My Site")),
      body(
        h1("Hello!"),
      ),
    ),
  ]
}

Layouts

Layouts are plain functions:

// shared/layout.ts
import "@hypeup/lexicon"

export default function layout(...content: Content[]) {
  return [
    doctype.html5,
    html(
      head(
        meta({ charset: "UTF-8" }),
        title("My Site"),
      ),
      body(content),
    ),
  ]
}

Used in page files:

// about.html.ts
import layout from "./shared/layout"

export default function About() {
  return layout(
    h1("About"),
    p("This is the about page."),
  )
}

Dynamic Routes

Parameterized routes use square brackets in the filename. Export a getStaticPaths function to provide the values at build time:

// [slug].html.ts
import layout from "./shared/layout"

export default function Post({ slug }: { slug: string }) {
  const post = getPost(slug)
  return layout(
    h1(post.title),
    p(post.body),
  )
}

export async function getStaticPaths() {
  return getAllPosts() // [{ slug: "hello" }, { slug: "world" }]
}

Dev Server

Use --watch to start a dev server with live reload:

hypeup generate --dir src --watch --port 5173

Options

hypeup generate [options]

  --dir <dir>    Directory to scan (default: ".")
  --out <dir>    Output directory (default: "dist")
  --clean        Remove output directory before generating
  --watch        Start dev server with live reload
  --port <port>  Dev server port (default: 5173)

Build Plugin

hypeup provides build plugins for using the global DSL in your app. Available for Vite, esbuild, Rollup, Rolldown, Bun, Farm, webpack, and Rspack:

// vite.config.ts
import hypeup from "@hypeup/plugin/vite"

export default {
  plugins: [hypeup()],
}

License

MIT