Overview
Quick Start
Installation
Core Concepts
Features
Supported Syntax & Limitations
Build Errors
Migration
Figma and Theme Integration
API
Devup

Features

Devup UI compiles the CSS-in-JS patterns you know from styled-components and Emotion at build time, with zero runtime overhead.

Syntax Coverage

Everything the build can know compiles to static CSS; a runtime value on an element becomes a CSS variable, and anything else is a build error naming the file, line and code. See Supported Syntax & Limitations for the full list.

How It Works

Devup UI transforms your components at build time. Class names are generated using compact base-37 encoding for minimal CSS output.

1// You write:
2const example = <Box bg="red" p={4} />
3
4// Devup UI generates:
5const generated = <div className="a b" />
6
7// CSS output:
8// .a { background-color: red; }
9// .b { padding: 16px; }  /* 4 * 4 = 16px */
1// You write:
2const example = <Box bg="red" p={4} />
3
4// Devup UI generates:
5const generated = <div className="a b" />
6
7// CSS output:
8// .a { background-color: red; }
9// .b { padding: 16px; }  /* 4 * 4 = 16px */

Note: Numeric values are multiplied by 4 to convert to pixels. p={4} becomes padding: 16px.

Class name sequence: a, b, ... z, _, aa, ab, ... az, a0, ... a9, a_, ba, ...

Responsive Design

Full responsive support with array syntax. Each index corresponds to a breakpoint.

1// You write:
2const example = <Box bg={['red', 'blue', 'green']} />
3
4// Devup UI generates:
5const generated = <div className="a b c" />
6
7// CSS output with media queries for each breakpoint
1// You write:
2const example = <Box bg={['red', 'blue', 'green']} />
3
4// Devup UI generates:
5const generated = <div className="a b c" />
6
7// CSS output with media queries for each breakpoint

Dynamic Values

Variables are automatically converted to CSS custom properties.

1// You write:
2const example = <Box bg={colorVariable} />
3
4// Devup UI generates:
5const generated = <div className="a" style={{ '--a': colorVariable }} />
6
7// CSS output:
8// .a { background-color: var(--a); }
1// You write:
2const example = <Box bg={colorVariable} />
3
4// Devup UI generates:
5const generated = <div className="a" style={{ '--a': colorVariable }} />
6
7// CSS output:
8// .a { background-color: var(--a); }

A value imported from another module stays static when that module declares it with const — a string or number literal, a member of an object literal (as const included), an alias of another constant, a re-export or a default export:

1// tokens.ts
2export const PRIMARY = 'red'
3export const colors = { brand: '#123456' } as const
4
5// App.tsx
6import { colors, PRIMARY } from './tokens'
7const example = <Box bg={colors.brand} color={PRIMARY} />
8
9// Devup UI generates:
10const generated = <div className="a b" />
1// tokens.ts
2export const PRIMARY = 'red'
3export const colors = { brand: '#123456' } as const
4
5// App.tsx
6import { colors, PRIMARY } from './tokens'
7const example = <Box bg={colors.brand} color={PRIMARY} />
8
9// Devup UI generates:
10const generated = <div className="a b" />

The same holds for a const declared at the top of the file itself, and templates or arithmetic over such constants fold into one value (`${SIZE * 2}px`).

Imports are resolved like the bundler resolves them — relative paths, tsconfig paths and packages (exports, module, main) — and a change to the module rebuilds the files reading it. CommonJS modules count too when they assign the export once (exports.PRIMARY = 'red', module.exports = { ... }). Anything that can change or is computed — a let, a property assigned twice, a function call — stays a CSS variable.

Conditional Expressions

Ternary operators and logical expressions are statically analyzed.

1// You write:
2const example = <Box bg={isActive ? 'blue' : dynamicColor} />
3
4// Devup UI generates:
5const generated = (
6  <div className={isActive ? 'a' : 'b'} style={{ '--b': dynamicColor }} />
7)
1// You write:
2const example = <Box bg={isActive ? 'blue' : dynamicColor} />
3
4// Devup UI generates:
5const generated = (
6  <div className={isActive ? 'a' : 'b'} style={{ '--b': dynamicColor }} />
7)

Complex Expressions

Combine responsive arrays with conditionals — Devup UI handles it all.

1// You write:
2const example = <Box bg={['red', 'blue', isActive ? 'green' : dynamicColor]} />
3
4// Devup UI generates:
5const generated = (
6  <div
7    className={`a b ${isActive ? 'c' : 'd'}`}
8    style={{ '--d': dynamicColor }}
9  />
10)
1// You write:
2const example = <Box bg={['red', 'blue', isActive ? 'green' : dynamicColor]} />
3
4// Devup UI generates:
5const generated = (
6  <div
7    className={`a b ${isActive ? 'c' : 'd'}`}
8    style={{ '--d': dynamicColor }}
9  />
10)

Pseudo Selectors

Use underscore prefix for pseudo selectors: _hover, _focus, _active, _disabled, and more.

1// You write:
2const example = (
3  <Box
4    _active={{ bg: 'red' }}
5    _focus={{ bg: 'green' }}
6    _hover={{ bg: 'blue' }}
7    bg="white"
8  />
9)
10
11// Devup UI generates:
12const generated = <div className="a b c d" />
13
14// CSS output:
15// .a { background-color: white; }
16// .b:hover { background-color: blue; }
17// .c:focus { background-color: green; }
18// .d:active { background-color: red; }
1// You write:
2const example = (
3  <Box
4    _active={{ bg: 'red' }}
5    _focus={{ bg: 'green' }}
6    _hover={{ bg: 'blue' }}
7    bg="white"
8  />
9)
10
11// Devup UI generates:
12const generated = <div className="a b c d" />
13
14// CSS output:
15// .a { background-color: white; }
16// .b:hover { background-color: blue; }
17// .c:focus { background-color: green; }
18// .d:active { background-color: red; }

Responsive + Pseudo Selectors

Combine responsive arrays with pseudo selectors for powerful responsive interactions.

1// You write:
2const example = <Box _hover={{ bg: ['red', 'blue'] }} />
3
4// Equivalent syntax:
5const example2 = <Box _hover={[{ bg: 'red' }, { bg: 'blue' }]} />
1// You write:
2const example = <Box _hover={{ bg: ['red', 'blue'] }} />
3
4// Equivalent syntax:
5const example2 = <Box _hover={[{ bg: 'red' }, { bg: 'blue' }]} />

Type-Safe Theming

Define themes in devup.json with full TypeScript support.

1{
2  "theme": {
3    "colors": {
4      "default": {
5        "primary": "#0070f3",
6        "text": "#000"
7      },
8      "dark": {
9        "primary": "#3291ff",
10        "text": "#fff"
11      }
12    },
13    "typography": {
14      "heading": {
15        "fontFamily": "Pretendard",
16        "fontSize": "24px",
17        "fontWeight": 700,
18        "lineHeight": 1.3
19      }
20    }
21  }
22}
1{
2  "theme": {
3    "colors": {
4      "default": {
5        "primary": "#0070f3",
6        "text": "#000"
7      },
8      "dark": {
9        "primary": "#3291ff",
10        "text": "#fff"
11      }
12    },
13    "typography": {
14      "heading": {
15        "fontFamily": "Pretendard",
16        "fontSize": "24px",
17        "fontWeight": 700,
18        "lineHeight": 1.3
19      }
20    }
21  }
22}
1// Type-safe theme tokens with autocomplete
2const textExample = <Text color="$primary" />
3const boxExample = <Box typography="heading" />
1// Type-safe theme tokens with autocomplete
2const textExample = <Text color="$primary" />
3const boxExample = <Box typography="heading" />

Theme switching happens via CSS variables — zero JavaScript execution required.

styled() API

Familiar API for styled-components and Emotion users.

1import { styled } from '@devup-ui/react'
2
3const Card = styled('div', {
4  bg: 'white',
5  p: 4, // 4 * 4 = 16px
6  borderRadius: '8px',
7  boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
8  _hover: {
9    boxShadow: '0 10px 15px rgba(0, 0, 0, 0.1)',
10  },
11})
12
13const Button = styled('button', {
14  bg: 'blue',
15  color: 'white',
16  px: 4, // 4 * 4 = 16px
17  py: 2, // 2 * 4 = 8px
18  borderRadius: '4px',
19  cursor: 'pointer',
20  _hover: { bg: 'darkblue' },
21  _active: { bg: 'navy' },
22})
1import { styled } from '@devup-ui/react'
2
3const Card = styled('div', {
4  bg: 'white',
5  p: 4, // 4 * 4 = 16px
6  borderRadius: '8px',
7  boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
8  _hover: {
9    boxShadow: '0 10px 15px rgba(0, 0, 0, 0.1)',
10  },
11})
12
13const Button = styled('button', {
14  bg: 'blue',
15  color: 'white',
16  px: 4, // 4 * 4 = 16px
17  py: 2, // 2 * 4 = 8px
18  borderRadius: '4px',
19  cursor: 'pointer',
20  _hover: { bg: 'darkblue' },
21  _active: { bg: 'navy' },
22})

styled() renders any base: a tag (styled.div, styled('div')), a Devup UI component (styled(Box)), a component or a member naming one (styled(Link), styled(motion.div)), or any other value (styled(forwardRef(...)), styled(tag)), which is read once where the component is declared. A base that can never render — null, a number, a boolean, undefined — and a styled the build cannot compile are build errors.

css() API

Create reusable style objects.

1import { css } from '@devup-ui/react'
2
3const cardStyles = css({
4  bg: 'white',
5  p: 4, // 4 * 4 = 16px
6  borderRadius: '8px',
7})
8
9const example = <Box className={cardStyles} />
1import { css } from '@devup-ui/react'
2
3const cardStyles = css({
4  bg: 'white',
5  p: 4, // 4 * 4 = 16px
6  borderRadius: '8px',
7})
8
9const example = <Box className={cardStyles} />

css(), globalCss() and keyframes() have no element to set a CSS variable on, so their values must be known at build time — literals, theme tokens or constants. Constants may be objects, arrays and TypeScript enums, declared in the file or imported: css(baseStyles), { ...baseStyles, color: 'red' }, _hover: hoverStyles, space[2], Size.M, and <Box {...baseStyles} _hover={hoverStyles} /> all read as if written in place, a later property replacing an earlier one as in JavaScript. Math calls over them (Math.max(SIZE, 20), Math.round(x), Math.PI) fold like arithmetic.

The build also runs code, but only code whose result is certain: what this file declares — consts, functions and enums — computing from literals and constants with exact built-ins (String, Number, JSON, Object, Array, string and array methods such as 'a'.toUpperCase() or [1, 2].map(double), and the exact Math functions abs, ceil, floor, round, trunc, sign, max, min, sqrt, fround, imul, clz32 and its constants). const double = (n) => n * 2; css({ w: double(SIZE) }) becomes a static class. An import is only read as the value its module declares — a literal, or an object or array of them — and the code of another module never runs, so darken(0.1, PRIMARY) with an imported darken is not computed. Anything less certain is not run: other globals (window, Date, Intl, IntersectionObserver...), Math.random and the approximate Math functions (sin, pow, exp...), **, toString with a radix, locale and Unicode methods (toLocaleString, localeCompare, normalize), this, new, classes, regular expressions, try, getters, async code and generators, JSX, a method chosen at runtime (obj[key]()), and a function writing a module-level binding. An element or a styled() component then keeps such a value as a CSS variable, as it keeps any runtime value; css(), globalCss() and keyframes() have no element for it, so it is a build error naming it — move the value to a style prop, or make it a constant. A whole style object computed that way (css(makeStyles()), styled.div(makeStyles())) is a build error too.

An object, array or enum constant reads as written only while no code the build sees changes it: the file using it, the module declaring it and the modules re-exporting it, CommonJS modules included. Assigning a member, delete, ++, a method such as push or sort, Object.assign, changing its elements in a for...of loop or a forEach/map callback, calling a method of it that uses this, or handing it to a function the build does not know (or to an object that is changed) makes it a value only known at runtime — an element reads its members as CSS variables, and css(), styled() or a spread taking it whole, directly or through a namespace import or an object holding it, is a build error that names where it is changed or handed on. What only reads it changes nothing: handing on a string or number it holds (darken(colors.primary)), keeping it in a const, export default or module.exports nothing changes, Object.freeze, Object.keys or Array.from over its primitives, and element props — React components must not change their props, so <ThemeProvider theme={theme}> only reads it; ref={obj} is the exception React assigns to. A module the build does not see can still change an imported object, so keep objects styles read unchanged at runtime, and use theme tokens or props for values that change.

A binding that css() or styled() composes reads the same way: with const base = makeStyles(), css(base), css(...parts) and styled.div(base) apply the rules the module computes, while a class held in a binding (const base = css({ ... }), a class imported from a stylesheet) still composes as a class. Elements take such objects too — <Box {...cardStyles} />, <Box _hover={hoverStyles} /> — as do a spread, a selector or a computed key in a styled object; values an element holds at runtime stay CSS variables. Source maps point at the code as written, not at the values computed in its place.

A condition the build knows picks its branch (const DARK = true; css({ color: DARK ? 'white' : 'black' })). globalCss(), keyframes() and <Global> have no class to switch, so a condition known only at runtime is a build error there, where css() and style props switch classes.

A value known only at runtime — a prop, a state, a parameter — is a build error naming it, as is styles the build cannot read where an object is expected: a spread of an unknown object, a selector such as _hover given something other than an object, a computed key. For a runtime value, use a style prop (<Box as={Component} w={width} />); any expression works there, await and new included, and runs as written. A JSX spread of an unknown object (<Box {...props} />) still passes its props at runtime.

Build errors point at the code they are about, and a file with several problems reports them all at once, in source order:

src/App.tsx:6:18: `css()` cannot use `width` at build time: its values must be literals, theme tokens or constants, or be computed from them
src/App.tsx:9:27: `globalCss()` cannot use `color` at build time: its values must be literals, theme tokens or constants, or be computed from them

globalCss() API

Define global styles that are extracted at build time.

1import { globalCss } from '@devup-ui/react'
2
3globalCss({
4  body: { margin: 0, fontFamily: 'Pretendard' },
5  '*': { boxSizing: 'border-box' },
6})
1import { globalCss } from '@devup-ui/react'
2
3globalCss({
4  body: { margin: 0, fontFamily: 'Pretendard' },
5  '*': { boxSizing: 'border-box' },
6})

Theme tokens work as in any style (body: { color: '$text' }). imports takes URLs or { url, query } objects, emitted as @import rules; an '@layer name' key puts the rules it holds in that cascade layer, and layers nest ('@layer base': { '@layer reset': { ... } } is base.reset):

1globalCss({
2  imports: ['reset.css', { url: 'print.css', query: 'print' }],
3  '@layer utilities': { '.hidden': { display: 'none' } },
4})
1globalCss({
2  imports: ['reset.css', { url: 'print.css', query: 'print' }],
3  '@layer utilities': { '.hidden': { display: 'none' } },
4})

keyframes() API

Define CSS animations.

1import { keyframes } from '@devup-ui/react'
2
3const spin = keyframes({
4  from: { transform: 'rotate(0deg)' },
5  to: { transform: 'rotate(360deg)' },
6})
7
8const example = <Box animation={`${spin} 1s linear infinite`} />
1import { keyframes } from '@devup-ui/react'
2
3const spin = keyframes({
4  from: { transform: 'rotate(0deg)' },
5  to: { transform: 'rotate(360deg)' },
6})
7
8const example = <Box animation={`${spin} 1s linear infinite`} />

A const holding what keyframes() or css() gives is a value the build knows, so the other style functions read it too: css({ animationName: spin }), globalCss`body { animation: ${spin} 1s; }`, css({ selectors: { ['.' + card + ':hover &']: { m: 1 } } }). The package imported whole works the same (import * as Devup from '@devup-ui/react' with Devup.keyframes(...), Devup.styled.div(...), <Devup.Box />). A name imported from another module is not read this way, as the class another file generates is only known once that file is built: declare the animation in the file using it, or read it from a style prop.

The build removes the imports it compiles, so their bindings exist only where they are called or rendered. A top-level alias (const myCss = css, const Row = Flex) compiles like the original and is removed; any other read — exporting it, putting it in an array, styled('div') without styles, an alias inside a function — is a build error, instead of code that would throw at runtime.