SKILL.md
name: devup-ui description: | Zero-runtime CSS-in-JS preprocessor for React. Transforms JSX styles to static CSS at build time.
TRIGGER WHEN:
- Writing/modifying Devup UI components (Box, Flex, Grid, Text, Button, etc.)
- Using styling APIs: css(), globalCss(), keyframes()
- Configuring devup.json theme (colors, typography, length, shadow, extends)
- Setting up build plugins (Vite, Next.js, Webpack, Rsbuild, Bun)
- Debugging "Cannot run on the runtime" errors
- Working with responsive arrays, pseudo-selectors (_hover, _dark, etc.)
- Using polymorphic
asprop orselectorsprop - Working with @devup-ui/components (Button, Input, Select, Toggle, etc.)
- Using responsive length tokens ($containerX, $gutter) or shadow tokens ($card, $sm)
Devup UI
Build-time CSS extraction. No runtime JS for styling.
Critical: Components Are Compile-Time Only
All @devup-ui/react components throw Error('Cannot run on the runtime'). They are placeholders that build plugins transform to native HTML elements with classNames.
// BEFORE BUILD (what you write):
<Box bg="red" p={4} _hover={{ bg: "blue" }} />
// AFTER BUILD (what runs in browser):
<div className="a b c" /> // + CSS: .a{background:red} .b{padding:16px} .c:hover{background:blue}
Components
@devup-ui/react (Layout Primitives)
All are polymorphic (accept as prop). Default element is <div> unless noted.
| Component | Default Element | Purpose |
|---|---|---|
Box |
div |
Base layout primitive, accepts all style props |
Flex |
div |
Flexbox container (shorthand for display: flex) |
Grid |
div |
CSS Grid container |
VStack |
div |
Vertical stack (flex column) |
Center |
div |
Centered content |
Text |
p |
Text/typography |
Image |
img |
Image element |
Input |
input |
Input element |
Button |
button |
Button element |
ThemeScript |
-- | SSR theme hydration (add to <head>) |
@devup-ui/components (Pre-built UI)
Higher-level components with built-in behavior. These are runtime components (not compile-time only).
| Component | Key Props |
|---|---|
Button |
variant (primary/default), size (sm/md/lg), loading, danger, icon, colors |
Checkbox |
children (label), onChange(checked), colors |
Input |
error, errorMessage, allowClear, icon, typography, colors |
Textarea |
error, errorMessage, typography, colors |
Radio |
variant (default/button), colors |
RadioGroup |
options[], direction (row/column), variant, value, onChange |
Toggle |
variant (default/switch), value, onChange(boolean), colors |
Select |
type (default/radio/checkbox), options[], value, onChange, colors |
Stepper |
min, max, type (input/text), value, onValueChange |
Select compound: SelectTrigger, SelectContainer, SelectOption, SelectDivider
Stepper compound: StepperContainer, StepperDecreaseButton, StepperIncreaseButton, StepperInput
Hooks: useSelect(), useStepper()
All components accept a colors prop object for runtime color customization via CSS variables.
Style Prop Syntax
Shorthand Props (ALWAYS prefer these)
Spacing (unitless number x 4 = px)
| Shorthand | CSS Property |
|---|---|
m, mt, mr, mb, ml, mx, my |
margin-* |
p, pt, pr, pb, pl, px, py |
padding-* |
Sizing
| Shorthand | CSS Property |
|---|---|
w |
width |
h |
height |
minW, maxW |
min-width, max-width |
minH, maxH |
min-height, max-height |
boxSize |
width + height (same value) |
Background
| Shorthand | CSS Property |
|---|---|
bg |
background |
bgColor |
background-color |
bgImage, bgImg, backgroundImg |
background-image |
bgSize |
background-size |
bgPosition, bgPos |
background-position |
bgPositionX, bgPosX |
background-position-x |
bgPositionY, bgPosY |
background-position-y |
bgRepeat |
background-repeat |
bgAttachment |
background-attachment |
bgClip |
background-clip |
bgOrigin |
background-origin |
bgBlendMode |
background-blend-mode |
Border
| Shorthand | CSS Property |
|---|---|
borderTopRadius |
border-top-left-radius + border-top-right-radius |
borderBottomRadius |
border-bottom-left-radius + border-bottom-right-radius |
borderLeftRadius |
border-top-left-radius + border-bottom-left-radius |
borderRightRadius |
border-top-right-radius + border-bottom-right-radius |
Layout & Position
| Shorthand | CSS Property |
|---|---|
flexDir |
flex-direction |
pos |
position |
positioning |
Helper: "top", "bottom-right", etc. (sets edges to 0) |
objectPos |
object-position |
offsetPos |
offset-position |
maskPos |
mask-position |
maskImg |
mask-image |
Typography
| Shorthand | Effect |
|---|---|
typography |
Applies theme typography token (fontFamily, fontSize, fontWeight, lineHeight, letterSpacing) |
All standard CSS properties from csstype are also accepted directly (e.g., display, gap, opacity, transform, animation, etc.).
Spacing Scale (unitless number x 4 = px)
<Box p={1} /> // padding: 4px
<Box p={4} /> // padding: 16px
<Box p="4" /> // padding: 16px (unitless string also x 4)
<Box p="20px" /> // padding: 20px (with unit = exact value)
Responsive Arrays (5 breakpoints)
// [mobile, mid, tablet, mid, PC] - 5 levels
// Use indices 0, 2, 4 most frequently. Use null to skip.
<Box bg={["red", null, "blue", null, "yellow"]} /> // mobile=red, tablet=blue, PC=yellow
<Box p={[2, null, 4, null, 6]} /> // mobile=8px, tablet=16px, PC=24px
<Box w={["100%", null, "50%"]} /> // mobile=100%, tablet+=50%
Pseudo-Selectors (underscore prefix)
<Box
_hover={{ bg: "blue" }}
_focus={{ outline: "2px solid blue" }}
_focusVisible={{ outlineColor: "$primary" }}
_active={{ bg: "darkblue" }}
_disabled={{ opacity: 0.5 }}
_before={{ content: '""' }}
_after={{ content: '""' }}
_firstChild={{ mt: 0 }}
_lastChild={{ mb: 0 }}
_placeholder={{ color: "gray" }}
/>
All CSS pseudo-classes and pseudo-elements from csstype are supported with _camelCase naming.
Group Selectors
Style children based on parent state:
<Box _groupHover={{ color: "blue" }} />
<Box _groupFocus={{ outline: "2px solid" }} />
<Box _groupActive={{ bg: "darkblue" }} />
Theme Selectors
<Box _themeDark={{ bg: "gray.900" }} />
<Box _themeLight={{ bg: "white" }} />
At-Rules (Media, Container, Supports)
// Underscore prefix syntax
<Box _print={{ display: "none" }} />
<Box _screen={{ display: "block" }} />
<Box _media={{ "(min-width: 768px)": { w: "50%" } }} />
<Box _container={{ "(min-width: 400px)": { p: 4 } }} />
<Box _supports={{ "(display: grid)": { display: "grid" } }} />
// @ prefix syntax (equivalent)
<Box {...{ "@media": { "(min-width: 768px)": { w: "50%" } } }} />
Custom Selectors
<Box selectors={{
"&:hover": { color: "red" },
"&::before": { content: '">"' },
"&:nth-child(2n)": { bg: "gray" },
}} />
Dynamic Values = CSS Variables
// Static value -> class
<Box bg="red" /> // className="a" + .a{background:red}
// Dynamic value -> CSS variable
<Box bg={props.color} /> // className="a" style={{"--a":props.color}} + .a{background:var(--a)}
// Conditional -> preserved
<Box bg={isActive ? "blue" : "gray"} /> // className={isActive ? "a" : "b"}
Responsive + Pseudo Combined
<Box _hover={{ bg: ['red', 'blue'] }} />
// Alternative syntax:
<Box _hover={[{ bg: 'red' }, { bg: 'blue' }]} />
Special Props
as (Polymorphic Element)
Changes the rendered HTML element or renders a custom component:
<Box as="section" bg="gray" /> // renders <section>
<Box as="a" href="/about" /> // renders <a>
<Box as={MyComponent} bg="red" /> // renders <MyComponent> with extracted styles
<Box as={b ? "div" : "section"} /> // conditional element type
props (Pass-Through to as Component)
When as is a custom component, use props to pass component-specific props:
<Box as={MotionDiv} w="100%" props={{ animate: { duration: 1 } }} />
styleVars (Manual CSS Variable Injection)
<Box styleVars={{ "--custom-color": dynamicValue }} bg="var(--custom-color)" />
styleOrder (CSS Cascade Priority)
Controls specificity when combining className with direct props. Required when mixing css() classNames with inline style props.
<Box className={cardStyle} bg="$background" styleOrder={1} />
// Conditional styleOrder
<Box bg="red" styleOrder={isActive ? 1 : 0} />
Styling APIs
css() Returns className String (NOT object)
import { css, globalCss, keyframes } from "@devup-ui/react";
import clsx from "clsx";
// css() returns a className STRING
const cardStyle = css({ bg: "white", p: 4, borderRadius: "8px" });
<div className={cardStyle} />
// Combine with clsx
const baseStyle = css({ p: 4, borderRadius: "8px" });
const activeStyle = css({ bg: "$primary", color: "white" });
<Box className={clsx(baseStyle, isActive && activeStyle)} styleOrder={1} />
globalCss() and keyframes()
globalCss({ body: { margin: 0 }, "*": { boxSizing: "border-box" } });
const spin = keyframes({ from: { transform: "rotate(0)" }, to: { transform: "rotate(360deg)" } });
<Box animation={`${spin} 1s linear infinite`} />
Dynamic Values with Custom Components
css() only accepts static values. For dynamic values on custom components, use <Box as={Component}>:
// WRONG - css() cannot handle dynamic values
<CustomComponent className={css({ w: width })} />
// CORRECT - Box with as prop handles dynamic values via CSS variables
<Box as={CustomComponent} w={width} />
Theme (devup.json)
{
"extends": ["./base-theme.json"],
"theme": {
"colors": {
"default": { "primary": "#0070f3", "text": "#000", "bg": "#fff" },
"dark": { "primary": "#3291ff", "text": "#fff", "bg": "#111" }
},
"typography": {
"heading": {
"fontFamily": "Pretendard",
"fontSize": "24px",
"fontWeight": 700,
"lineHeight": 1.3,
"letterSpacing": "-0.02em"
},
"body": [
{ "fontSize": "14px", "lineHeight": 1.5 },
null,
{ "fontSize": "16px", "lineHeight": 1.6 }
]
},
"length": {
"default": {
"containerX": ["16px", null, "32px"],
"gutter": ["8px", null, "16px"]
}
},
"shadow": {
"default": {
"card": ["0 1px 2px #0003", null, null, "0 4px 8px #0003"],
"sm": "0 1px 2px rgba(0,0,0,0.05)"
}
}
}
}
- Colors: Use with
$prefix in JSX props:<Box color="$primary" /> - Typography: Use with
$prefix:<Text typography="$heading" /> - Length: Responsive length tokens:
<Box px="$containerX" />,<Flex gap="$gutter" /> - Shadow: Responsive shadow tokens:
<Box boxShadow="$card" /> - extends: Inherit from base config files (deep merge, last wins)
- Responsive typography/length/shadow: Use arrays with
nullfor unchanged breakpoints
Length & Shadow Token Behavior
Length and shadow tokens support responsive arrays like typography. The key distinction is how $token behaves depending on syntax:
| Syntax | Behavior | Classes |
|---|---|---|
px="$containerX" |
Expands to all defined breakpoints | Multiple |
px={"$containerX"} |
Expands to all defined breakpoints | Multiple |
px={["$containerX"]} |
Single value at index 0 only | 1 |
px={["8px", null, "$containerX"]} |
8px at index 0, token at index 2 |
2 |
Both "$token" and {"$token"} expand the responsive token. Only {["$token"]} inside a responsive array keeps it as a single class — because the array itself defines the breakpoint levels.
Theme types are auto-generated via module augmentation of DevupTheme and DevupThemeTypography.
Theme API
import { useTheme, setTheme, getTheme, initTheme, ThemeScript } from "@devup-ui/react";
setTheme("dark"); // Switch theme (sets data-theme + localStorage)
const theme = getTheme(); // Get current theme name
const theme = useTheme(); // React hook (reactive)
initTheme(); // Initialize on startup (auto-detect system preference)
<ThemeScript /> // SSR hydration script (add to <head>, prevents FOUC)
Build Plugin Setup
Vite
import DevupUI from "@devup-ui/vite-plugin";
export default defineConfig({ plugins: [react(), DevupUI()] });
Next.js
import { DevupUI } from "@devup-ui/next-plugin";
export default DevupUI({ /* Next.js config */ });
Rsbuild
import DevupUI from "@devup-ui/rsbuild-plugin";
export default defineConfig({ plugins: [DevupUI()] });
Webpack
import { DevupUIWebpackPlugin } from "@devup-ui/webpack-plugin";
// Add to plugins array
Bun
import { plugin } from "@devup-ui/bun-plugin";
// Auto-registers, always uses singleCss: true
Plugin Options
DevupUI({
singleCss: true, // Single CSS file (recommended for Turbopack)
include: ["@devup/hello"], // Process external libs using @devup-ui
prefix: "du", // Class name prefix (e.g., "du-a" instead of "a")
debug: true, // Enable debug logging
importAliases: { // Redirect imports from other CSS-in-JS libs
"@emotion/styled": "styled", // default: enabled
"styled-components": "styled", // default: enabled
"@vanilla-extract/css": true, // default: enabled
},
})
$token Scope
$token values (colors, length, shadow) only work in JSX props. Use var(--token) in external objects.
// CORRECT - $token in JSX prop
<Box bg="$primary" />
<Box px="$containerX" />
<Box boxShadow="$card" />
<Box bg={{ active: '$primary', inactive: '$gray' }[status]} />
// WRONG - $token in external object (won't be transformed)
const colors = { active: '$primary' }
<Box bg={colors.active} /> // broken!
// CORRECT - var(--token) in external object
const colors = { active: 'var(--primary)' }
<Box bg={colors.active} />
Inline Variant Pattern (Preferred)
Use inline object indexing instead of external config objects:
// PREFERRED - inline object indexing (build-time extractable)
<Box
h={{ lg: '48px', md: '40px', sm: '32px' }[size]}
bg={{ primary: '$primary', secondary: '$gray100' }[variant]}
/>
// AVOID - external config object (becomes dynamic, uses CSS variables)
const sizeStyles = { lg: { h: '48px' }, md: { h: '40px' } }
<Box h={sizeStyles[size].h} />
Anti-Patterns (NEVER do)
| Wrong | Right | Why |
|---|---|---|
<Box style={{ color: "red" }}> |
<Box color="red"> |
style prop bypasses extraction |
<Box {...css({...})} /> |
<Box className={css({...})} /> |
css() returns string, not object |
css({ bg: variable }) |
<Box bg={variable}> or <Box as={Comp} bg={variable}> |
css()/globalCss() only accept static values |
$color in external object |
var(--color) in external object |
$color only transformed in JSX props |
| No build plugin configured | Configure plugin first | Components throw at runtime without transformation |
as any on style props |
Fix types properly | Type errors indicate real issues |
@ts-ignore / @ts-expect-error |
Fix the type issue | Suppression hides real problems |
background="red" |
bg="red" |
Always use shorthands |
padding={4} |
p={4} |
Always use shorthands |
width="100%" |
w="100%" |
Always use shorthands |
styled("div", {...}) |
<Box bg="red" /> |
Use Box component with props, not styled() |
stylex.create({...}) |
<Box bg="red" /> |
Use Box component with props, not stylex |
README
The Future of CSS-in-JS — Zero Runtime, Full Power
Zero Config · Zero FOUC · Zero Runtime · Complete CSS-in-JS Syntax Coverage
English | 한국어
Why Devup UI?
Devup UI isn't just another CSS-in-JS library — it's the next evolution.
Traditional CSS-in-JS solutions force you to choose between developer experience and performance. Devup UI eliminates this trade-off entirely by processing all styles at build time using a Rust-powered preprocessor.
- Complete Syntax Coverage: Every CSS-in-JS pattern you know — variables, conditionals, responsive arrays, pseudo-selectors — all fully supported
- Familiar API:
styled()API compatible with styled-components and Emotion patterns - True Zero Runtime: No JavaScript execution for styling at runtime. Period.
- Smallest Bundle Size: Optimized class names (
a,b, ...aa,ab) minimize CSS output - Fastest Build Times: Rust + WebAssembly delivers unmatched preprocessing speed
Install
npm install @devup-ui/react
# on next.js
npm install @devup-ui/next-plugin
# on vite
npm install @devup-ui/vite-plugin
# on rsbuild
npm install @devup-ui/rsbuild-plugin
# on webpack
npm install @devup-ui/webpack-plugin
Features
- Preprocessor — All CSS extraction happens at build time
- Zero Config — Works out of the box with sensible defaults
- Zero FOUC — No flash of unstyled content, no Provider required
- Zero Runtime — No client-side JavaScript for styling
- RSC Support — Full React Server Components compatibility
- Library Mode — Build component libraries with extracted styles
- Dynamic Themes — Zero-cost theme switching via CSS variables
- Type-Safe Themes — Full TypeScript support for theme tokens
- Smallest & Fastest — Proven by benchmarks
Comparison Benchmarks
Next.js Build Time and Build Size (github action - ubuntu-latest)
| Library | Version | Build Time | Build Size |
|---|---|---|---|
| tailwindcss | 4.1.13 | 19.31s | 59,521,539 bytes |
| styleX | 0.15.4 | 41.78s | 86,869,452 bytes |
| vanilla-extract | 1.17.4 | 19.50s | 61,494,033 bytes |
| kuma-ui | 1.5.9 | 20.93s | 69,924,179 bytes |
| panda-css | 1.3.1 | 20.64s | 64,573,260 bytes |
| chakra-ui | 3.27.0 | 28.81s | 222,435,802 bytes |
| mui | 7.3.2 | 20.86s | 97,964,458 bytes |
| devup-ui(per-file css) | 1.0.18 | 16.90s | 59,540,459 bytes |
| devup-ui(single css) | 1.0.18 | 17.05s | 59,520,196 bytes |
| tailwindcss(turbopack) | 4.1.13 | 6.72s | 5,355,082 bytes |
| devup-ui(single css+turbopack) | 1.0.18 | 10.34s | 4,772,050 bytes |
How it works
Devup UI transforms your components at build time. Class names are generated using a compact base-37 encoding for minimal CSS size.
Basic transformation:
// You write:
const variable = <Box _hover={{ bg: 'blue' }} bg="red" p={4} />
// Devup UI generates:
const variable = <div className="a b c" />
// With CSS:
// .a { background-color: red; }
// .b { padding: 1rem; }
// .c:hover { background-color: blue; }
Dynamic values become CSS variables:
// You write:
const example = <Box bg={colorVariable} />
// Devup UI generates:
const example = <div className="a" style={{ '--a': colorVariable }} />
// With CSS:
// .a { background-color: var(--a); }
Complex expressions and responsive arrays — fully supported:
// You write:
const example = <Box bg={['red', 'blue', isActive ? 'green' : dynamicColor]} />
// Devup UI generates:
const example = (
<div
className={`a b ${isActive ? 'c' : 'd'}`}
style={{ '--d': dynamicColor }}
/>
)
// With responsive CSS for each breakpoint
Type-safe theming:
devup.json
{
"theme": {
"colors": {
"default": {
"primary": "#0070f3",
"text": "#000"
},
"dark": {
"primary": "#3291ff",
"text": "#fff"
}
},
"typography": {
"heading": {
"fontFamily": "Pretendard",
"fontSize": "24px",
"fontWeight": 700,
"lineHeight": 1.3
}
}
}
}
// Type-safe theme tokens
const textExample = <Text color="$primary" />
const boxExample = <Box typography="$heading" />
Responsive + Pseudo selectors together:
// Responsive with pseudo selector
const example = <Box _hover={{ bg: ['red', 'blue'] }} />
// Equivalent syntax
const example2 = <Box _hover={[{ bg: 'red' }, { bg: 'blue' }]} />
styled-components / Emotion compatible styled() API:
import { styled } from '@devup-ui/react'
// Familiar syntax for styled-components and Emotion users
const Card = styled('div', {
bg: 'white',
p: 4, // 4 * 4 = 16px
borderRadius: '8px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
_hover: {
boxShadow: '0 10px 15px rgba(0, 0, 0, 0.1)',
},
})
const Button = styled('button', {
px: 4, // 4 * 4 = 16px
py: 2, // 2 * 4 = 8px
borderRadius: '4px',
cursor: 'pointer',
})
// Usage
const cardExample = <Card>Content</Card>
const buttonExample = <Button>Click me</Button>
Inspirations
- Styled System
- Chakra UI
- Theme UI
- Vanilla Extract
- Rainbow Sprinkles
- Kuma UI
How to Contribute
Requirements
Development Setup
To set up the development environment, install the following packages:
bun install
bun run build
cargo install cargo-tarpaulin
cargo install wasm-pack
After installation, run bun run test to ensure everything works correctly.