# Forge Guide A complete guide to writing components with Forge. ## Table of Contents - [Getting Started](#getting-started) - [Basic Components](#basic-components) - [HTML Tags](#html-tags) - [CSS Properties](#css-properties) - [States](#states) - [Variants](#variants) - [Parts](#parts) - [Custom Render](#custom-render) - [Selectors](#selectors) - [Scopes](#scopes) - [Themes](#themes) - [SSR and SPA](#ssr-and-spa) --- ## Getting Started Import `define` and start creating components. Each call to `define` generates real CSS classes and returns a JSX component. ```tsx import { define } from 'forge' const Box = define('Box', { padding: 20, background: '#111', }) // Renders:
...
Hello ``` Include `` in your HTML head to render the generated CSS: ```tsx import { define, Styles } from 'forge' // or use define.Styles — same thing ... ``` --- ## Basic Components ### Named components Pass a name as the first argument. The name becomes the CSS class. ```tsx const Card = define('Card', { padding: 20, background: '#111', borderRadius: 8, }) //
...
``` Names must be unique — defining the same name twice throws an error. ### Anonymous components Omit the name and Forge generates one from the base tag: `Div`, `Button2`, `Anchor3`, etc. ```tsx const Box = define({ display: 'flex', gap: 16 }) // class="Div" const Link = define({ base: 'a', color: 'blue' }) // class="Anchor" ``` --- ## HTML Tags By default, components render as `
`. Use `base` to change the tag: ```tsx const Button = define('Button', { base: 'button', padding: 20, cursor: 'pointer', }) // const Heading = define('Heading', { base: 'h1', fontSize: 28, }) //

...

``` ### Attribute shorthand Set default attributes right in the base string: ```tsx const Radio = define('Radio', { base: 'input[type=radio]', }) // const Checkbox = define('Checkbox', { base: 'input[type=checkbox]', }) // ``` Props passed at usage time override base attributes. --- ## CSS Properties Write CSS properties in camelCase. They compile to real CSS at definition time. ```tsx const Card = define('Card', { display: 'flex', flexDirection: 'column', gap: 16, padding: 20, backgroundColor: '#111', borderRadius: 8, boxShadow: '0 2px 4px rgba(0,0,0,0.1)', }) ``` Generated CSS: ```css .Card { background-color: #111; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); display: flex; flex-direction: column; gap: 16px; padding: 20px; } ``` ### Numeric values Numbers are auto-converted to `px`: ```tsx { padding: 20 } // padding: 20px { margin: 0 } // margin: 0px { fontSize: 14 } // font-size: 14px { borderRadius: 8 } // border-radius: 8px ``` Except for unitless properties, which stay as plain numbers: ```tsx { opacity: 0.5 } // opacity: 0.5 { zIndex: 10 } // z-index: 10 { flex: 1 } // flex: 1 { flexGrow: 2 } // flex-grow: 2 { flexShrink: 0 } // flex-shrink: 0 { fontWeight: 400 } // font-weight: 400 { lineHeight: 1.6 } // line-height: 1.6 { order: 3 } // order: 3 ``` Strings are passed through as-is: ```tsx { padding: '12px 24px' } // padding: 12px 24px { border: '1px solid #222' } // border: 1px solid #222 { margin: '0 auto' } // margin: 0 auto ``` ### All supported properties Every standard CSS property is supported in camelCase form: layout (`display`, `position`, `flexDirection`, `gridTemplateColumns`, etc.), box model (`margin`, `padding`, `width`, `height`, etc.), typography (`fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, etc.), visual (`background`, `border`, `boxShadow`, `opacity`, etc.), animation (`transition`, `animation`, `transform`, etc.), and SVG (`fill`, `stroke`, `strokeWidth`, etc.). --- ## States Pseudo-selectors like `:hover`, `:focus`, `:active`, and `:disabled`. ```tsx const Button = define('Button', { base: 'button', padding: 20, background: 'blue', cursor: 'pointer', states: { ':hover': { background: 'darkblue' }, ':active': { transform: 'translateY(1px)' }, ':focus': { outline: '2px solid white' }, ':disabled': { opacity: 0.3, cursor: 'not-allowed' }, }, }) ``` The colon is optional — `hover` and `:hover` are equivalent: ```tsx states: { hover: { background: 'darkblue' }, // same as ':hover' } ``` ### Complex pseudo-selectors Use the full string for compound selectors: ```tsx states: { ':not(:disabled):hover': { background: 'darkblue' }, ':not(:disabled):active': { transform: 'translateY(1px)' }, } ``` Generated CSS: ```css .Button:not(:disabled):hover { background: darkblue; } .Button:not(:disabled):active { transform: translateY(1px); } ``` --- ## Variants Variants are typed props that apply conditional CSS classes. They replace inline styles with a clean, declarative API. ### Boolean variants A variant whose value is a style object. Activated by passing `true`. ```tsx const Button = define('Button', { base: 'button', padding: 20, background: 'blue', variants: { disabled: { opacity: 0.3, cursor: 'not-allowed', }, rounded: { borderRadius: 999, }, }, }) ``` ```tsx ``` Generated CSS: ```css .Button { padding: 20px; background: blue; } .Button.disabled { cursor: not-allowed; opacity: 0.3; } .Button.rounded { border-radius: 999px; } ``` HTML output for ` ``` Variant props are consumed by Forge and **not** passed to the HTML element. ### Keyed variants A variant whose value is an object of named options. Activated by passing a string. ```tsx const Button = define('Button', { base: 'button', padding: 16, variants: { intent: { primary: { background: 'blue', color: 'white' }, secondary: { background: '#333', color: '#ccc' }, danger: { background: 'red', color: 'white' }, ghost: { background: 'transparent', color: 'gray' }, }, size: { small: { padding: '8px 16px', fontSize: 12 }, large: { padding: '16px 32px', fontSize: 16 }, }, }, }) ``` ```tsx ``` Generated CSS: ```css .Button.intent-primary { background: blue; color: white; } .Button.intent-danger { background: red; color: white; } .Button.size-small { font-size: 12px; padding: 8px 16px; } .Button.size-large { font-size: 16px; padding: 16px 32px; } ``` ### Variants with states Variants can include their own pseudo-selectors: ```tsx variants: { intent: { danger: { background: 'red', states: { ':not(:disabled):hover': { background: '#cc0000' }, }, }, secondary: { background: '#333', states: { ':not(:disabled):hover': { borderColor: 'green' }, }, }, }, } ``` ### Combining multiple variants Multiple keyed and boolean variants can be used together freely: ```tsx ``` --- ## Parts Parts are named sub-components within a component. They get their own CSS classes and can have their own base tags, states, and selectors. ```tsx const Card = define('Card', { padding: 20, background: '#111', parts: { Header: { base: 'h2', fontSize: 24, marginBottom: 12, }, Body: { fontSize: 14, lineHeight: 1.6, color: '#888', }, Footer: { base: 'footer', marginTop: 16, paddingTop: 12, borderTop: '1px solid #333', fontSize: 12, }, }, render({ props, parts: { Root, Header, Body, Footer } }) { return (
{props.title}
{props.children}
{props.footer}
) }, }) ``` ```tsx This is the card body content. ``` Generated CSS classes: ```css .Card { padding: 20px; background: #111; } .Card_Header { font-size: 24px; margin-bottom: 12px; } .Card_Body { color: #888; font-size: 14px; line-height: 1.6; } .Card_Footer { border-top: 1px solid #333; font-size: 12px; margin-top: 16px; padding-top: 12px; } ``` ### Parts with states Parts can have their own pseudo-selectors: ```tsx parts: { Tab: { base: 'button', color: '#888', borderBottom: '1px solid transparent', states: { ':hover': { color: '#fff' }, }, }, } ``` ### Variants that affect parts Variants can override styles on specific parts: ```tsx const UserProfile = define('UserProfile', { padding: 24, parts: { Avatar: { base: 'img', width: 64, height: 64, borderRadius: '50%' }, Name: { fontSize: 18 }, Bio: { fontSize: 14, color: '#888' }, }, variants: { size: { compact: { padding: 16, // override root parts: { Avatar: { width: 48, height: 48 }, // override part Name: { fontSize: 16 }, }, }, large: { padding: 32, parts: { Avatar: { width: 96, height: 96 }, Name: { fontSize: 24 }, }, }, }, verified: { parts: { Avatar: { border: '2px solid gold' }, }, }, }, render({ props, parts: { Root, Avatar, Name, Bio } }) { return ( {props.name}{props.verified && ' ✓'} {props.bio} ) }, }) ``` ```tsx ``` Generated CSS: ```css .UserProfile { padding: 24px; } .UserProfile_Avatar { border-radius: 50%; height: 64px; width: 64px; } .UserProfile.size-compact { padding: 16px; } .UserProfile_Avatar.size-compact { height: 48px; width: 48px; } .UserProfile_Avatar.verified { border: 2px solid gold; } ``` ### Applying variants to parts in render Inside `render()`, you can pass variant props directly to part components: ```tsx const Tabs = define('Tabs', { display: 'flex', parts: { Tab: { base: 'button', color: '#888', borderBottom: '1px solid transparent', }, }, variants: { active: { parts: { Tab: { color: 'green', borderBottom: '1px solid green', }, }, }, }, render({ props, parts: { Root, Tab } }) { return ( {props.items?.map((item: any) => ( {item.label} ))} ) }, }) ``` The `active` prop on `` adds the variant class to that specific tab instance. It is not passed through to the HTML. --- ## Custom Render Override the default rendering with a `render` function. It receives `props` (everything passed to the component) and `parts` (component functions for Root and all named parts). ```tsx const FormGroup = define('FormGroup', { marginBottom: 24, parts: { Label: { base: 'label', display: 'block', fontSize: 14, marginBottom: 8 }, Helper: { fontSize: 12, color: '#888', marginTop: 6 }, Error: { fontSize: 12, color: 'red', marginTop: 6 }, }, render({ props, parts: { Root, Label, Helper, Error } }) { return ( {props.label && } {props.children} {props.helper && {props.helper}} {props.error && {props.error}} ) }, }) ``` ```tsx ``` ### Destructuring props Destructure to separate custom props from HTML passthrough props: ```tsx render({ props: { title, subtitle, ...rest }, parts: { Root, H2, P } }) { return (

{title}

{subtitle}

) } ``` ### Without render If no `render` is provided, the component renders its children into the root tag: ```tsx const Box = define('Box', { padding: 20 }) // Equivalent to: // render({ props, parts: { Root } }) { // return {props.children} // } ``` --- ## Selectors The `selectors` key lets you write custom CSS selectors. Use `&` for the current element and `@PartName` to reference other parts. ### Basic selectors ```tsx const NavLink = define('NavLink', { base: 'a', color: '#888', textDecoration: 'none', selectors: { '&:hover': { color: '#fff' }, '&[aria-current]': { color: '#fff', textDecoration: 'underline' }, }, }) ``` ```tsx Home About ``` ### Cross-part selectors Reference other parts with `@PartName`. This is the mechanism that enables CSS-only interactive components. ```tsx const Checkbox = define('Checkbox', { parts: { Input: { base: 'input[type=checkbox]', display: 'none', }, Label: { base: 'label', padding: 10, cursor: 'pointer', color: 'gray', selectors: { // When the Input is checked, style this Label '@Input:checked + &': { color: 'green', fontWeight: 'bold', }, // When the Input is disabled, style this Label '@Input:disabled + &': { opacity: 0.5, cursor: 'not-allowed', }, }, }, }, render({ props, parts: { Root, Input, Label } }) { return ( ) }, }) ``` The `@Input` in `@Input:checked + &` is replaced with `.Checkbox_Input`, and `&` is replaced with `.Checkbox_Label`, producing: ```css .Checkbox_Input:checked + .Checkbox_Label { color: green; font-weight: bold; } ``` ### CSS-only tab switcher Hidden radio inputs + sibling selectors = tabs without JavaScript: ```tsx const TabSwitcher = define('TabSwitcher', { parts: { Input: { base: 'input[type=radio]', display: 'none', }, TabBar: { display: 'flex', borderBottom: '1px solid #333', }, TabLabel: { base: 'label', padding: '12px 24px', color: '#888', cursor: 'pointer', states: { ':hover': { color: '#fff' }, }, selectors: { '@Input:checked + &': { color: 'green', borderBottom: '1px solid green', }, }, }, Content: { display: 'none', padding: 24, selectors: { // General sibling combinator: when a radio is checked, // show the corresponding content panel '@Input:checked ~ &': { display: 'block' }, }, }, }, render({ props, parts: { Root, Input, TabBar, TabLabel, Content } }) { return ( {props.tabs?.map((tab: any, i: number) => ( <> {tab.label} ))} {props.tabs?.map((tab: any) => ( {tab.content} ))} ) }, }) ``` ```tsx First panel

}, { id: 'two', label: 'Tab 2', content:

Second panel

}, { id: 'three', label: 'Tab 3', content:

Third panel

}, ]} /> ``` ### Selector reference | Selector | Meaning | |---|---| | `&:hover` | This element on hover | | `&[aria-current]` | This element when attribute is present | | `@Input:checked + &` | This element when adjacent Input is checked | | `@Input:checked ~ &` | This element when any preceding Input sibling is checked | | `@Input:disabled + &` | This element when adjacent Input is disabled | | `@Input:checked + &:hover` | This element on hover, but only when Input is checked | --- ## Scopes When building a family of related components, `createScope` prefixes all names automatically: ```tsx import { createScope } from 'forge' const { define } = createScope('Button') const Button = define('Root', { // CSS class: "Button" (Root is special) base: 'button', padding: 20, }) const ButtonRow = define('Row', { // CSS class: "ButtonRow" display: 'flex', gap: 16, }) const ButtonIcon = define('Icon', { // CSS class: "ButtonIcon" width: 20, height: 20, }) ``` `Root` is the special case — `define('Root', ...)` with scope `'Button'` produces class `Button`, not `ButtonRoot`. --- ## Themes Built-in CSS custom properties with type safety. ### Define themes Create a theme file with your tokens: ```tsx // darkTheme.tsx export default { 'colors-bg': '#0a0a0a', 'colors-bgElevated': '#111', 'colors-fg': '#00ff00', 'colors-fgMuted': '#888', 'colors-border': '#222', 'colors-accent': '#00ff00', 'colors-accentDim': '#008800', 'fonts-mono': "'Monaco', 'Menlo', monospace", 'spacing-sm': '12px', 'spacing-md': '16px', 'spacing-lg': '24px', 'spacing-xl': '32px', 'radius-sm': '4px', 'radius-md': '8px', } as const ``` ```tsx // lightTheme.tsx export default { 'colors-bg': '#f5f5f0', 'colors-bgElevated': '#fff', 'colors-fg': '#0a0a0a', 'colors-fgMuted': '#666', 'colors-border': '#ddd', 'colors-accent': '#0066cc', 'colors-accentDim': '#004499', 'fonts-mono': "'Monaco', 'Menlo', monospace", 'spacing-sm': '12px', 'spacing-md': '16px', 'spacing-lg': '24px', 'spacing-xl': '32px', 'radius-sm': '4px', 'radius-md': '8px', } as const ``` ### Register themes ```tsx // themes.tsx import { createThemes } from 'forge' import darkTheme from './darkTheme' import lightTheme from './lightTheme' export const theme = createThemes({ dark: darkTheme, light: lightTheme, }) ``` `createThemes` returns a typed function. It only accepts keys that exist in your theme objects. ### Use theme values `theme('key')` returns `var(--theme-key)`: ```tsx import { theme } from './themes' const Card = define('Card', { padding: theme('spacing-lg'), // var(--theme-spacing-lg) background: theme('colors-bgElevated'), // var(--theme-colors-bgElevated) color: theme('colors-fg'), // var(--theme-colors-fg) border: `1px solid ${theme('colors-border')}`, borderRadius: theme('radius-md'), }) ``` ### Switch themes Themes are controlled by the `data-theme` attribute: ```tsx // Set initial theme // Switch at runtime document.body.setAttribute('data-theme', 'light') ``` Generated CSS: ```css [data-theme="dark"] { --theme-colors-bg: #0a0a0a; --theme-colors-fg: #00ff00; /* ... */ } [data-theme="light"] { --theme-colors-bg: #f5f5f0; --theme-colors-fg: #0a0a0a; /* ... */ } ``` ### Other theme APIs ```tsx // Register a single theme import { createTheme } from 'forge' createTheme('dark', { bgColor: '#000', fgColor: '#fff' }) // Extend existing themes with new tokens import { extendThemes } from 'forge' extendThemes({ dark: { 'colors-success': '#0f0' } }) // Untyped fallback for dynamic theme keys import { themeVar } from 'forge' themeVar('colors-bg') // 'var(--theme-colors-bg)' ``` --- ## SSR and SPA ### Server-side rendering Include `` in the `` to inline all CSS: ```tsx import { define, Styles } from 'forge' const Page = () => ( ... ) ``` Or write CSS to a file using `stylesToCSS()`: ```tsx import { stylesToCSS } from 'forge' // Write to a .css file and serve it const css = stylesToCSS() Bun.write('public/main.css', css) // Then link it ``` ### Single-page apps In browser environments, Forge automatically injects a `