Library / SDK
segmentio/ui-box avatar
segmentio/ui-box

ui-box: a React primitive that turns props into CSS

Blazing Fast React UI Primitive

1,061 stars53 forksTypeScriptMIT

At a glance

What is it?
ui-box is a low level CSS-in-JS library for React where every CSS property is a prop on a single Box component, so a reusable component can be given a margin without a wrapper element or a class. It is MIT licensed, the current release is v5.4.1 from 2023-03-23, and the last push was on 2026-06-25.
Who is it for?
ui-box fits a React codebase that wants CSS-in-JS without a theming layer or a component suite on top, and that has a real use for the `is` prop and the `selectors` prop to build small reusable primitives. It does not fit a team that wants a design system, a styled-components alternative with theming built in, or a runtime that keeps up with the current React version, since the peer range stops at React 18 and the last tagged release is from March 2023.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 119 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One component, and CSS properties as props

ui-box is a low level CSS-in-JS solution that focuses on being simple, fast and extensible, and all CSS properties are set using simple React props, which allows you to easily create reusable components that can be enhanced with additional CSS properties.

The stated benefit of that design is the wrapper problem. Adding a margin to a component would normally require adding non-reusable wrapper elements or classes, and ui-box removes both by making the styling part of the component's own props.

The usage example is short enough to be the whole API:

jsx
import Box from 'ui-box'

function Button(props) {
  return <Box is="button" padding="10px" background="red" {...props} />
}

function Example() {
  return (
    <Button disabled margin="10px">
      Hi
    </Button>
  )
}

The result renders a red, disabled button with margins. The `is` prop changes the underlying element type to `button`, the padding and background are CSS properties passed as props, `disabled` is passed through to the DOM element, and the spread of `props` after the styling means a caller can override any of it.

The `is` prop accepts either a string to change the DOM element type or a React component type to inherit another component, with the only requirement that the component accepts a `className` prop. The documented example is inheriting the react-router `Link` component:

jsx
<Box is={Link} to="/login">
  Login
</Box>

That `className` requirement is the contract that makes the whole thing work, and it is worth checking against your own components before you build on the library, because a component that does not forward `className` will silently lose its styling when used through `is`.

The selectors prop covers hover and focus without a second library

The `selectors` prop is the feature that takes ui-box beyond a props-to-CSS mapping, and it is what the documentation points at for richer interactions.

The prop allows you to define selectors and custom styles to apply when the selector condition is met, and it can be used to create richer element interactions, such as hover or focus states, without the use of another CSS-in-JS library.

tsx
<Box is="a" selectors={{ '&:hover': { cursor: 'pointer', textDecoration: 'underline' } }}>
  Hello world
</Box>

The shape of the value is `object<selector: string, props: CssProps>`, so the keys are CSS selectors, with the `&` nesting syntax the example uses, and the values are the same CSS property props the rest of the API uses. The fact that the values are props rather than raw CSS values is the detail that keeps the API consistent: a `textDecoration` here means the same thing as a `textDecoration` prop on the Box itself.

Two other smaller props round out the element API. `clearfix` is a boolean utility property for easily adding clearfix styles to the element. And `className` is described with some affection, as the className prop you know and love, with the note that internally it gets enhanced with additional class names for the CSS properties you specify. That is the whole CSS-in-JS model in one sentence: the consumer supplies a class name, the library appends its own generated ones.

The `boxSizing` property is set to `border-box` by default, which is a deviation from the CSS default and the kind of detail that saves an afternoon of debugging a box that is four pixels wider than expected.

And the CSS property list is long, over a hundred entries covering layout, flexbox, grid, typography, borders, transitions, animation and the SVG presentation properties like `fill`, `stroke` and `strokeWidth`. Values can be passed as a string or as a number, and the number is converted to a `px` value. The shorthand properties with repeated values only accept a single value, so `margin="10px"` works but `margin="10px 20px"` does not, and the x and y props, `marginX` and `marginY`, achieve the same thing.

Server rendering needs an extract and hydrate cycle

The functions outside the component are what make ui-box usable in a server-rendered application, and they are a three-part API.

`extractStyles()` returns a `{ cache, styles }` object containing the cache entries and the rendered styles for server rendering. The styles can be output in a `<style>` tag or an external stylesheet, however you want. The cache should be passed to `hydrate()` on the client side before mounting the app.

`hydrate(cache)` hydrates the cache using the `cache` value returned from `extractStyles()`, and it is used to prevent needing to recalculate all the class names and re-render all the styles on page load when server rendering.

`clearStyles()` clears the cache and removes the rendered styles, and it is mainly useful for resetting the global state when using `extractStyles()` in unit tests. The documentation is explicit about calling it after each test.

That cache is global state, and the three functions are the whole API for managing it. A component library with a cache that has to be seeded on the server, shipped to the client and cleared between tests is not a trivial design, and the fact that the documentation tells you the exact order is a sign that the order matters.

Two utility functions round out the exports. `splitProps(props, keys)` filters out props based on an array of keys and returns a `{ matchedProps, remainingProps }` object, and `splitBoxProps(props)` does the same for Box props specifically. These exist for a practical reason: a component that spreads props onto a Box needs to separate the ones the Box understands from the ones it does not, and doing that by hand in every component is exactly the boilerplate a primitive is supposed to remove.

There is also a `keyframes(name, timeline)` function for generating an animation keyframe name and injecting the provided styles into the stylesheet, which is how the animation properties on the Box get their keyframes.

Three runtime dependencies and a deliberate absence of theming

The dependency list in `package.json` is short and explains what the library does and does not do.

There are three runtime dependencies. `@emotion/hash` at ^0.7.1 generates the hash used in the generated class names, `inline-style-prefixer` at ^5.0.4 handles vendor prefixing of the inline styles, and `prop-types` at ^15.7.2 provides the runtime prop type checking.

The peer dependency is React, with a range of `^16.0.0 || ^17.0.0 || ^18.0.0`. The engines field requires Node 12 or newer, and the package declares `sideEffects: false`, which lets a bundler drop unused modules.

What is absent is the important part. There is no theming layer, no theme provider, no styled component, no variant system, no design token support. This is a low level primitive, and the positioning in the readme is consistent: it focuses on being simple, fast and extensible.

That is a real difference from the other CSS-in-JS approaches, and it cuts both ways. A team that has already built a theming system and a component suite on top of a different primitive can use ui-box as the bottom layer, because it does not impose a theme API on top. A team that wanted a theming solution will find that ui-box is not one, and that they have to build the theme layer themselves.

The build and test tooling shows the era. The build is `tsc`, producing `dist/src` with the declaration files, and the tests are `xo && nyc ava`, with AVA for the test runner, nyc for coverage and xo for linting. There is a `.storybook/` directory with a `dev` script running Storybook on port 9009 and a `build-storybook` script, and a `benchmark` script running `react-benchmark` against `tools/benchmarks/box.ts`, plus a `size` script using `size-limit`, which is a bundle size budget check. A library that claims to be fast tends to measure itself, and this one has both a benchmark harness and a size limit configured.

The version story is the main thing to weigh

The release history is where an evaluation of this library has to slow down.

The most recent tagged releases are v5.4.1 from 2023-03-23, v5.4.0 from 2022-11-29 and v5.3.0 from 2022-11-15. The last push to the repository was on 2026-06-25, so something has changed since the last tag, but there has been no new release since March 2023.

The peer dependency range ending at React 18 is the concrete symptom. React 19 has been out long enough that a library still declaring `^16.0.0 || ^17.0.0 || ^18.0.0` is not being tested against it, and the version range itself is the contract you install against. A project on React 19 with a strict peer dependency check will get a warning or an error on install, and a project that ignores peer warnings is running an unstated combination.

There is a second, subtler issue. The library keeps global state, the style cache, and the class name generation depends on a hash of the property values. The `hydrate` and `extractStyles` cycle exists to keep the server and client in step, and that cycle is exactly the kind of machinery that breaks when a new React version changes how rendering is scheduled. The last release predates React 19 by a wide margin, so the SSR path has not been validated against the current renderer.

The contributors list is another signal, and a hopeful one. Five named contributors, with Jeroen Ransijn, Roland Warmerdam, Meichen Zhou, Netto Farah and Matt Shwery, and a repository under the segmentio organisation, which is the Segment.io team. The project was maintained by people whose day job was a product team's front end, which explains both the quality of the implementation and the absence of a release cadence.

Where ui-box is the wrong choice

The first case is a team that wants a CSS-in-JS solution with theming. ui-box is a primitive, not a system. If your requirement includes a theme provider, dark mode switching driven by tokens, variants, or a consistent spacing scale enforced across the app, you are building that layer yourself on top of a library that has no opinion about it.

The second case is a project already standardised on a CSS-in-JS library with a large ecosystem. Introducing a second one to avoid a wrapper element for margins is a poor trade, because you now have two style systems, two class name generators and two sets of opinions about specificity, and the duplication the library avoids is trivial next to that.

The third is a React 19 project, or a project that intends to move to it, and that uses server rendering. The peer range stops at React 18, the last release is from 2023, and the SSR path depends on a global cache that has to be extracted on the server and hydrated on the client. A mismatch there produces a flash of unstyled content or a hydration error, and diagnosing it means reading the library's internals rather than a version changelog.

The fourth is a project that has moved to a different styling model altogether. Tailwind with utility classes, or a build-time CSS approach, solves the same problem, and the utility model makes the wrapper problem disappear in a different way, by not needing reusable styled components at all.

There is one more consideration for a library that describes itself as fast. The `size-limit` script and the `react-benchmark` harness mean the author measured, which is good, but the measurements are from the 2023 era and the versions of React, of the bundler and of the toolchain have all moved since. The claim that the library is fast is credible; the claim that it is still fast in your application is something you would have to measure yourself, and the harness in `tools/benchmarks/` is there for you to run.

Editorial conclusion

ui-box fits a React codebase that wants CSS-in-JS without a theming layer or a component suite on top, and that has a real use for the `is` prop and the `selectors` prop to build small reusable primitives. It does not fit a team that wants a design system, a styled-components alternative with theming built in, or a runtime that keeps up with the current React version, since the peer range stops at React 18 and the last tagged release is from March 2023. Before you adopt it, check the `is` prop against your own component conventions, because inheriting another component requires that it accept a `className` prop.

Frequently asked questions

How do I install ui-box?

Run `yarn add ui-box` or `npm install --save ui-box`. The peer dependency is React with a range of `^16.0.0 || ^17.0.0 || ^18.0.0`, and Node 12 or newer is required.

What does the is prop do in ui-box?

It changes the underlying element type. You can pass a string to change the DOM element, or a React component type to inherit another component, in which case that component only needs to accept a `className` prop, as the react-router `Link` example shows.

How do I use ui-box with server rendering?

Call `extractStyles()` on the server to get a `{ cache, styles }` object, output the styles in a `<style>` tag or external stylesheet, and pass the cache to `hydrate()` on the client before mounting. Call `clearStyles()` between unit tests that use `extractStyles()`.

Does ui-box support hover and focus states?

Yes, through the `selectors` prop, which takes an object of selector strings to CSS props, for example `selectors={{ '&:hover': { cursor: 'pointer', textDecoration: 'underline' } }}`. The documentation describes it as a way to create richer element interactions without another CSS-in-JS library.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. segmentio/ui-box on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/segmentio-ui-box.svg)](https://hysenlabs.com/projects/segmentio-ui-box)

Was this useful?