@kbach/react
v0.5.42
Published
React / React Native components and hooks for the Kbach framework
Readme
@kbach/react
Tailwind-like utility classes for React (web). Classes are resolved to inline styles at render time through a custom JSX runtime — no separate stylesheet step required. Responsive, dark mode, hover, and pseudo-element rules are injected as real CSS so they work with the full CSS cascade.
/** @jsxImportSource @kbach/react */
<div className="bg-white dark:bg-gray-10 p-4 rounded-xl shadow" />
<div className="bg-blue-7 hover:bg-blue-8 dark:bg-indigo-6 rounded-lg px-6 py-3" />
<div className="bg-[#6366f1] p-[14px] rounded-[20px]" />
<div className="group">
<span className="opacity-0 group-hover:opacity-100 transition" />
</div>Install
npm install @kbach/reactSetup
1. Configure the JSX runtime
tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@kbach/react"
}
}Vite
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { kbach } from '@kbach/react/vite';
export default defineConfig({
plugins: [
react({ jsxImportSource: '@kbach/react' }),
kbach(), // generates CSS and writes it to your main CSS file
],
});The kbach() Vite plugin scans your source files for class strings (including ternary expressions, template literals, and clsx/cn call patterns) and writes the generated CSS between /* kbach:start */ / /* kbach:end */ markers in your main CSS file (src/index.css, app/app.css, etc.). On HMR only the changed file is rescanned — unchanged class tokens reuse their cached CSS.
Babel
// babel.config.js
module.exports = {
presets: [
['@babel/preset-react', { runtime: 'automatic', importSource: '@kbach/react' }],
],
};Per-file pragma (no config change required)
/** @jsxImportSource @kbach/react */2. Wrap your app
import { ThemeProvider } from '@kbach/react';
export default function Root() {
return (
<ThemeProvider defaultMode="system">
<App />
</ThemeProvider>
);
}API
className / kb prop
Works on any HTML element or React component once the JSX runtime is configured. Both className and kb are equivalent — kb is shorter and avoids conflicts with library components that forward className to their own DOM elements.
<div className="bg-white dark:bg-gray-10 p-4 rounded-xl" />
<p kb="text-gray-10 dark:text-white text-lg font-bold" />
<button className="bg-blue-7 hover:bg-blue-8 pressed:bg-blue-9 rounded-lg px-4 py-2" />
<input className="caret-blue-6 focus:ring-2" />styled(Component, classes)
Pre-style any component. Returns a new component that merges the base classes with any kb prop passed at use time. On web, the class string is forwarded as className so CSS rules (group-hover:, before:, print:, etc.) match the element.
import { styled } from '@kbach/react';
const Card = styled('div', 'bg-white dark:bg-gray-9 rounded-2xl p-6 shadow');
const Title = styled('h2', 'text-2xl font-bold text-gray-10 dark:text-white');
const Button = styled(
'button',
'bg-blue-7 hover:bg-blue-8 dark:bg-indigo-6 rounded-xl px-6 py-3'
);
// Pass extra classes at use time
<Card kb="mt-4 mb-2">
<Title kb="text-3xl">Hello</Title>
</Card>useStyles(classes, state?)
Resolve classes to a style object inside a component. Accepts a string, an array of strings (merged left-to-right), and an optional interaction state object.
import { useStyles } from '@kbach/react';
// Basic
const style = useStyles('bg-blue-6 dark:bg-indigo-6 px-3 py-1 rounded-full');
// Multiple class strings merged
const style = useStyles(['bg-white p-4', 'dark:bg-gray-9 rounded-xl']);
// With interaction state
const [pressed, setPressed] = useState(false);
const style = useStyles('bg-blue-5 pressed:bg-blue-7 rounded-lg', { pressed });useResolvedStyle(classes)
Returns the raw bucket map ({ base, dark, hover, … }) without flattening. Useful when you need to pass different buckets to different elements, or animate between states with Animated.
const resolved = useResolvedStyle('bg-white dark:bg-gray-9 p-4');
// { base: { backgroundColor: '#fff', padding: 16 }, dark: { backgroundColor: '#1f2937' } }kb(classes)
Resolve classes outside a component (static contexts, StyleSheet.create(), etc.).
import { kb } from '@kbach/react';
const cardStyle = kb('bg-white p-4 rounded-xl') as React.CSSProperties;cx(...classes)
Conditionally join class strings. Falsy values are ignored.
import { cx } from '@kbach/react';
<div className={cx(
'p-4 rounded-xl',
isSelected && 'border-2 border-blue-6',
isDisabled && 'opacity-50',
)} />useTheme()
import { useTheme } from '@kbach/react';
const { mode, resolvedMode, isDark, setMode, toggle, config } = useTheme();| Value | Type | Description |
|---|---|---|
| mode | 'light' \| 'dark' \| 'system' | User-selected mode |
| resolvedMode | 'light' \| 'dark' | Resolved after system lookup |
| isDark | boolean | Shorthand for resolvedMode === 'dark' |
| setMode | fn | Set mode explicitly |
| toggle | fn | Toggle between light and dark |
| config | ResolvedConfig | Full resolved config (theme, darkMode, plugins) |
useIsDark()
import { useIsDark } from '@kbach/react';
const isDark = useIsDark();useBreakpoint() / useResponsive()
import { useBreakpoint, useResponsive } from '@kbach/react';
const bp = useBreakpoint(); // 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
const { sm, md, lg } = useResponsive();
const padding = lg ? 32 : sm ? 16 : 8;useColors()
Returns the active theme's color palette as a smart proxy. Supports shade access, /opacity modifier, and arbitrary CSS color manipulation. Handles 3, 4, 6, and 8-digit hex values.
import { useColors } from '@kbach/react';
const colors = useColors();
colors.blue[6] // '#3b82f6'
colors.blue['6/50'] // 'rgba(59,130,246,0.5)'
colors.white // '#ffffff'
colors['white/20'] // 'rgba(255,255,255,0.2)'
colors.alpha('#ff6b35', 60) // 'rgba(255,107,53,0.6)'ThemeToggle
Drop-in toggle component.
<ThemeToggle /> // button (default)
<ThemeToggle variant="switch" /> // toggle switch
<ThemeToggle variant="icon-button" /> // icon button
<ThemeToggle variant="button" includeSystem /> // three-way selectorModifiers
Modifiers are chainable with no hard cap — as many as needed in any order:
<div className="dark:sm:hover:focus:p-4" />
<div className="dark:group-hover:not-disabled:text-blue-6" />Theme & mode
| Modifier | Trigger |
|---|---|
| dark: | Dark mode active |
| not-dark: / light: | Light mode active |
| not-light: | Dark mode active (alias) |
Interactive — JS state + CSS pseudo
| Modifier | Trigger |
|---|---|
| hover: / not-hover: | Mouse hover |
| focus: / not-focus: | Focused |
| pressed: / not-pressed: | Click / touch down |
| active: / not-active: | Active state |
| disabled: / not-disabled: | Disabled |
| checked: / not-checked: | Checkbox / radio checked |
| visited: / not-visited: | Visited link |
| placeholder: | Placeholder text |
CSS-only pseudo-classes (structural)
| Modifier | CSS |
|---|---|
| first: | :first-child |
| last: | :last-child |
| odd: | :nth-child(odd) |
| even: | :nth-child(even) |
| only: | :only-child |
| focus-within: | :focus-within |
| focus-visible: | :focus-visible |
Pseudo-elements
<div className="before:content-['*'] before:text-red-6 relative" />
<p className="first-letter:text-4xl first-letter:font-bold" />
<input className="placeholder:text-gray-5" />
<p className="selection:bg-blue-3" />| Modifier | CSS |
|---|---|
| before: | ::before |
| after: | ::after |
| selection: | ::selection |
| first-letter: | ::first-letter |
| first-line: | ::first-line |
| marker: | ::marker |
| placeholder: | ::placeholder |
Group / peer ancestor selectors
<div className="group">
<span className="opacity-0 group-hover:opacity-100 transition" />
</div>
<input className="peer" />
<p className="peer-focus:text-blue-6" />| Modifier | Fires when |
|---|---|
| group-hover: | Ancestor .group is hovered |
| group-focus: | Ancestor .group is focused |
| peer-hover: | Previous sibling .peer is hovered |
| peer-focus: | Previous sibling .peer is focused |
Responsive
| Modifier | Min-width |
|---|---|
| sm: | 576 px |
| md: | 768 px |
| lg: | 1024 px |
| xl: | 1280 px |
| 2xl: | 1536 px |
<div className="print:hidden" />
<div className="print:text-black print:bg-white" />Orientation & accessibility media
| Modifier | Media query |
|---|---|
| landscape: | (orientation: landscape) |
| portrait: | (orientation: portrait) |
| motion-reduce: | (prefers-reduced-motion: reduce) |
| motion-safe: | (prefers-reduced-motion: no-preference) |
| contrast-more: | (prefers-contrast: more) |
| contrast-less: | (prefers-contrast: less) |
Directionality
<div className="rtl:text-right ltr:text-left" />Important
Prefix any class with ! to add !important to every declaration it produces:
<div className="!p-0 !m-0" />Arbitrary values
<div className="bg-[#6366f1]" />
<div className="p-[14px]" />
<div className="text-[18px]" />
<div className="w-[calc(100%-2rem)]" />
<div className="bg-[rgba(99,102,241,0.15)]" />Color with opacity
<div className="bg-blue-6/50" /> // 50% opacity
<div className="bg-gray-10/75" /> // 75% opacityColor scale
Colors use a 12-shade scale — 1 is lightest, 12 is darkest.
shade 1 · 2 · 3 · 4 · 5 · 6 · 7 · 8 · 9 · 10 · 11 · 12
light ──────────────────────────────────────────────────────────────── darkAvailable families: slate, gray, zinc, neutral, stone, red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose.
Common utilities
| Category | Examples |
|---|---|
| Background | bg-white, bg-gray-10, bg-blue-6, bg-[#fff] |
| Text | text-sm, text-2xl, text-gray-10, font-bold, font-sans |
| Text wrap | text-balance, text-pretty, text-nowrap, text-wrap |
| Line clamp | line-clamp-3, line-clamp-none |
| Padding | p-4, px-6, py-3 |
| Margin | m-4, mx-auto, mt-2, -mt-4 |
| Size | size-12, w-full, h-screen, w-screen, w-[200px] |
| Flex | flex-1, flex-row, items-center, justify-between, gap-4 |
| Grid | grid-3, col-span-2, grid-flow-col |
| Border | border, border-2, border-gray-4, rounded-xl |
| Shadow | shadow, shadow-md, shadow-lg |
| Opacity | opacity-50, opacity-75 |
| Overflow | overflow-hidden, overflow-clip, overflow-x-auto |
| Cursor | cursor-pointer, cursor-grab, cursor-zoom-in |
| Scroll | scroll-smooth, scroll-auto |
| Float / clear | float-left, float-right, clear-both |
| Vertical align | align-middle, align-top, align-baseline |
| Touch action | touch-none, touch-pan-x, touch-manipulation |
| Caret | caret-blue-6, caret-transparent |
| Accent | accent-blue-6, accent-auto |
| Animation | animate-spin, animate-pulse, animate-bounce, animate-ping |
| Transition | transition, duration-300, delay-150 |
Configuration
// kbach.config.js
module.exports = {
darkMode: 'attribute', // 'attribute' | 'class' | 'media'
theme: {
// Replace a theme section entirely
colors: {
brand: { 1: '#eff6ff', 6: '#3b82f6', 10: '#1e3a5f' },
},
},
extend: {
// Add to defaults without replacing them
colors: { brand: { 6: '#6366f1' } },
spacing: { 18: '72px' },
screens: { '3xl': '1920px' },
},
plugins: [
({ addUtility, addVariant, theme }) => {
// Custom utility
addUtility('border-brand', {
borderColor: theme('colors.brand.6'),
borderWidth: 2,
});
// Custom variant — selector string (auto-converted to CSS rules)
addVariant('hocus', ':hover, :focus');
addVariant('supports-grid', '@media (display: grid)');
addVariant('dark-theme', '.dark-theme');
// Custom variant — full ModifierDef for JS-tracked interactive states
addVariant('long-press', {
pseudo: ':active',
jsBehavior: 'interactive',
jsMatch: (_, state) => !!state.longPressed,
});
},
],
};Color aliases
Color values can reference other palette entries — chains up to 5 levels deep are resolved:
extend: {
colors: {
primary: 'blue-6', // → resolved hex of blue shade 6
brand: { 6: 'primary' }, // → follows through to blue-6's hex
},
},Font Family
// kbach.config.js
module.exports = {
extend: {
fontFamily: {
sans: 'Inter_400Regular',
'sans-md': 'Inter_500Medium',
'sans-bold': 'Inter_700Bold',
},
},
};Use as utilities:
<p className="font-sans">Regular</p>
<p className="font-sans-md">Medium</p>
<p className="font-sans-bold">Bold</p>Global default font (web): When fontFamily.sans is set to anything other than 'System', Kbach automatically injects body { font-family: <your-font> } — all text inherits it without font-sans on every element.
Apply at runtime (e.g. from a server-side user preference):
import { updateConfig } from '@kbach/react';
updateConfig({ extend: { colors: { brand: { 6: '#6366f1' } } } });Custom responsive breakpoints
Add breakpoints in extend.screens — they automatically register as new modifier names:
// kbach.config.js
module.exports = {
extend: {
screens: { '3xl': '1920px' },
},
};<div className="3xl:max-w-screen-3xl 3xl:mx-auto" />Full reference
See kbach-react.md for the complete utility reference, all modifiers, configuration options, and common patterns.
For React Native / Expo, see the @kbach/native package.
