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.

In 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.

src/lib/style.ts
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.

src/button.ts
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:

index.html
<!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:

src/preview.ts
import { buttonClass } from './button'

document.querySelector<HTMLButtonElement>('#demo')!.className = buttonClass(
  'primary',
  'large'
)

Serve the HTML with Bun’s development server:

Terminal
bun --hot ./index.html

Open 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.

A class name is not a CSS rule

@hulla/style doesn’t generate CSS. If the class string looks right but the button is unstyled, check that your stylesheet defines those classes and is loaded by the page.

The Variants guide explains inferred prop types. Composition adds caller-provided classes, and Custom merging covers Tailwind conflict resolution.