Guide
Style a button
Give a button a choice of sizes and colors with typed class recipes.
On this page
Before you start
- Bun and a directory for a small TypeScript project.
Build a button with a choice of sizes and colors. Start by printing the class names, then use them on a web page with a little CSS.
In an empty project directory, initialize a package, then install @hulla/style.
bun init -ynpm init -ypnpm inityarn init -ybun add @hulla/style@0.4.0npm install @hulla/style@0.4.0pnpm add @hulla/style@0.4.0yarn add @hulla/style@0.4.0In src/lib/style.ts, create the cn and vn functions once and import them from components. cn accepts strings, arrays, and objects whose truthy keys should be included.
import { style } from '@hulla/style'
export const { cn, vn } = style()With the default configuration, cn('button', { disabled: false }) returns button. Repeated identical tokens are kept once, in their first-seen order.
In src/button.ts, declare style choices and derive the function’s input types from the maps. Here tone is required and size defaults to small.
import { cn, vn } from './lib/style'
const $tone = vn({
primary: 'button-primary',
quiet: 'button-quiet',
})
const $size = vn({ small: 'button-small', large: 'button-large' })
export function buttonClass(
tone: typeof $tone.infer,
size: typeof $size.infer = 'small'
) {
return cn('button', $tone(tone), $size(size))
}
console.log(buttonClass('primary'))Run bun src/button.ts. It prints button button-primary button-small. TypeScript rejects unknown tone and size names. Use the returned string as class in HTML or className in React.
Create index.html in the project root. The CSS defines the classes; @hulla/style only selects and combines them:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Button recipes</title>
<style>
.button {
border: 1px solid currentColor;
border-radius: 0.5rem;
cursor: pointer;
}
.button-primary {
background: #1746a2;
color: white;
}
.button-quiet {
background: transparent;
color: #1746a2;
}
.button-small {
padding: 0.5rem 0.75rem;
}
.button-large {
padding: 0.75rem 1.25rem;
}
</style>
</head>
<body>
<button id="demo" type="button">Try a recipe</button>
</body>
<script type="module" src="./src/preview.ts"></script>
</html>Create the module that uses the recipe:
import { buttonClass } from './button'
document.querySelector<HTMLButtonElement>('#demo')!.className = buttonClass(
'primary',
'large'
)Serve the HTML with Bun’s development server:
bun --hot ./index.htmlOpen the local URL printed in the terminal. The button should have a blue background and the larger padding. Change 'primary' to 'quiet': it becomes an outlined button. Change 'large' to 'huge' and your editor should report an invalid variant key.
The Variants guide explains inferred prop types. Composition adds caller-provided classes, and Custom merging covers Tailwind conflict resolution.