
Pure TypeScript UI Framework.
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.
hypeup generateHTML 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.
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.
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,
You can add CSS properties directly to elements:
div(
color.red,
fontWeight.bold,
"this is bold and red!",
)
Use rule() for CSS rules and prop() for custom properties.
style(
rule(".red-bold",
color.red,
fontWeight.bold,
prop("--some-custom", "value"),
),
)
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,
)
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,
),
)
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.
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 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.
The client runtime provides mounting and event handling for interactive applications.
import "@hypeup/lexicon"
import { mount } from "@hypeup/client"
function App() {
return div(
h1("Hello, world!"),
)
}
mount(document.getElementById("app")!, () => App())
Use on to bind event handlers:
button(
"Click me",
on("click", () => {
console.log("clicked!")
}),
)
Call redraw() after mutating state to update the page.
Use ref to get a reference to a DOM element:
const myInput = ref<HTMLInputElement>()
input(myInput, { type: "text" })
// later...
myInput.current?.focus()
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))
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 fileIf the build tool supports other languages, those work too (e.g. .html.civet).
hypeup generate --dir src --out dist
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.
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 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."),
)
}
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" }]
}
Use --watch to start a dev server with live reload:
hypeup generate --dir src --watch --port 5173
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)
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()],
}
MIT