Skip to main contentSkip to navigation

Introduction

Lux components, composed on @hanzo/gui. Style props and a real grid — no Tailwind build, no Radix, no shadcn underneath.

@luxfi/ui is the Lux component set. It is a thin Lux skin over @hanzo/gui, a style library for React and React Native that takes styles as props and compiles them to atomic CSS on web. Because the engine is cross-platform, so is the skin: the same Switch renders through a <div> on web and through a native view on a phone.

There is no utility-class engine here. No Tailwind build step, no tailwind.config.js, no Radix primitive underneath a component, and no CLI that copies source into your repository.

Install and render

npm install @luxfi/ui @hanzo/gui @tanstack/react-query
import { AppProvider } from '@luxfi/ui/provider'
import '@luxfi/ui/tokens.css'

export default function App() {
  return <AppProvider>{/* your app */}</AppProvider>
}

AppProvider is GuiProvider fed the Lux config, plus a TanStack QueryClientProvider. It is the one provider a Lux surface mounts. It also resolves the white label from the request host, so the same image serves lux.network, zoo.ngo, hanzo.ai and pars.id without a rebuild.

Web builds need the engine wired into the bundler — one plugin under Vite, one wrapper under Next. See Installation.

Styling is props

A style is a prop on the component, and a responsive style is a prop nested under a breakpoint key.

import { Button } from '@luxfi/ui/button'
import { XStack, YStack, Text } from '@luxfi/ui/gui'

<YStack gap={16} padding={24} $md={{ padding: 40 }}>
  <Text fontSize="$5" color={'var(--color-text-primary)' as never}>
    Balance
  </Text>
  <XStack gap={8}>
    <Button>Send</Button>
    <Button variant="outline">Receive</Button>
  </XStack>
</YStack>

Breakpoints are mobile-first min-widths and are named $sm $md $lg $xl ($xs 460, $sm 640, $md 768, $lg 1024, $xl 1280). Desktop-first reads $['max-md']. The gui documentation also shows $gtSm / $gtMd; those are the v4 config's names and this package is on v5 — gui forwards a media key it does not know straight to the DOM instead of matching it, so the rule never applies and nothing tells you.

Font size is a token, never a number: write fontSize="$3", not fontSize={14}. @hanzo/appearance lets a reader set their own type scale, and it works by multiplying the ramp that $n resolves through. A hardcoded 14 cannot move.

Grid is a real display: grid — @hanzo/ui/grid, not a flex row pretending.

Colour comes from a token

@luxfi/ui/tokens.css is the whole palette. It resolves in layers: an achromatic ladder (--lux-n-*), two brand hooks the white label overrides (--lux-accent, --lux-accent-fg), two state hues (--lux-state-error, --lux-state-success), and the semantic names components actually reach for (--color-*). A shadcn-compatible alias block (--background, --foreground, --primary …) sits at the end so a surface migrating in can delete its own palette and import this instead.

Lux is monochrome. A component may not contain a colour literal, and a ramp colour is not a token: bg-red-600 is a constant that cannot retune between themes. See Theming.

Three ways to write a class, if you still have classes

An app arriving from a utility engine carries class strings, and the engine here does not read them. Three converters do, and which one you use depends on what receives the string.

ReceiverConverterWritten
a host elementBox<Box className="flex gap-4">
a gui componentsx<Card {...sx('grid gap-4')}>
a component taking only stylecss<Icon style={css('size-4')} />

Box and sx read the string through tw and emit only the part tw did not understand, so a converted class never reaches the DOM. css returns flat CSSProperties, which means a breakpoint or a hover variant cannot ride it — those need Box or sx.

import { Box, sx, css } from '@luxfi/ui/utils'

Named lux-* classes are ours, for the handful of things a prop cannot express: a sibling combinator, an ancestor's pseudo-class, a keyframe. They are declared in tokens.css. A lux-* name that is not declared there styles nothing.

Three entries carry the layer

Everything else is a component subpath.

  • @luxfi/ui/gui re-exports the engine — styled, View, Text, XStack, Sheet, Theme, useMedia. It is a separate subpath because the skin and the engine both define Button, Input, Image, Switch, Separator, Select, Tabs, Avatar, Checkbox, Slider and Progress; one barrel would collide.
  • @luxfi/ui/white-label is resolveWhiteLabel(host), a pure function from a Host header to { org, name, domain, accent, theme, issuer, clientId }. It has no React and no DOM in it, so the SSR server, the browser and an edge rewrite all resolve the same row.
  • @luxfi/ui/vite is luxUi(), the gui-on-web bundler wiring as one plugin. Under Next the counterpart is withGui from @hanzo/ui/next.

Three packages, not one

@luxfi/ui 9.0.0 split what used to be one Tailwind/shadcn library into three. Which one you want depends on how the component is written, not on what it draws — see Packages.

FAQ