Styling with sx
Every kara component is styled through props, and every value is a token: p="s16", bg="surface.primary", borderRadius="large". One sx surface resolves to SwiftUI modifiers on iOS, Compose modifiers on Android, or a React Native style — so call sites never branch on the platform. A raw number or hex where a token belongs does not type-check.
One surface, three targets
<VStack bg="fill.accent.base" borderRadius="large" p="s16" wide> {/* SwiftUI / Compose modifiers */}
<Box bg="fill.accent.base" borderRadius="large" p="s16"> {/* a React Native style */}The two lines look alike and resolve differently: the native stack becomes a background, a clip and padding modifiers; the Box becomes a View style. Both read the same tokens from the same theme.
There are two surfaces, and they are not interchangeable. Native components take the native sx — only what SwiftUI and Compose both honour. Box and the React Native tier take the wider view sx, with flexbox, gap, margins and per-corner radii. gap on a VStack does not compile; it wants spacing.
Native sx
What every native component takes, with the same meaning on both platforms.
| Prop | Type | Description |
|---|---|---|
| bg | ColorToken | The background colour, a palette token. |
| blur | number | A Gaussian blur over the whole box, its radius in points. |
| borderColor | ColorToken | The stroke colour, with |
| borderRadius | RadiiSize | Rounds the corners — and clips the content, the background and the stroke to them. |
| borderWidth | number | A stroke inside the edge, in points. |
| h | number | A fixed height, in points. |
| minH | number | The least height, in points. |
| minW | number | The least width, in points. |
| offsetX | number | Moves the box sideways after layout, in points — its neighbours stay where they were. |
| offsetY | number | Moves the box up or down after layout, in points — its neighbours stay where they were. |
| opacity | OpacitySize | How opaque the whole box is, an opacity token. |
| p | SpaceSize | Padding on every side. A side or an axis of its own wins over it. |
| pb | SpaceSize | Bottom padding. |
| pl | SpaceSize | Left padding — the leading edge, on the native surface. |
| pr | SpaceSize | Right padding — the trailing edge, on the native surface. |
| pt | SpaceSize | Top padding. |
| px | SpaceSize | Left and right padding. |
| py | SpaceSize | Top and bottom padding. |
| rotate | number | Clockwise, in degrees. |
| scale | number | {x?: number; y?: number} | Uniform, or per axis — an omitted axis stays at 1. |
| shadow | ShadowName | One of the theme's shadows — a drop, an inset highlight and a hairline ring, as the token defines. |
| tall | boolean | Takes all the height the parent offers. |
| w | number | A fixed width, in points. |
| wide | boolean | Takes all the width the parent offers. |
| zIndex | ZIndexSize | Draws the box above or below its siblings, a z-index token. |
A Gaussian blur over the whole box, its radius in points.
Rounds the corners — and clips the content, the background and the stroke to them.
A stroke inside the edge, in points.
A fixed height, in points.
The least height, in points.
The least width, in points.
Moves the box sideways after layout, in points — its neighbours stay where they were.
Moves the box up or down after layout, in points — its neighbours stay where they were.
Clockwise, in degrees.
Uniform, or per axis — an omitted axis stays at 1.
One of the theme's shadows — a drop, an inset highlight and a hairline ring, as the token defines.
Takes all the height the parent offers.
A fixed width, in points.
Takes all the width the parent offers.
Draws the box above or below its siblings, a z-index token.
Platform buckets
What only one platform can express goes in its own bucket: ios for SwiftUI modifiers with no honest Compose counterpart, android for the reverse. A bucket is inert on the other platform rather than a type error, so a call site never has to branch.
<HStack
borderRadius="large"
ios={{glass: 'regular'}}
android={{weight: 1}}
p="s16"
/>ios
| Prop | Type | Description |
|---|---|---|
| aspectRatio | number | Width over height, fitted within the proposed size. The escape hatch covers |
| brightness | number | Brightens (positive) or darkens (negative) the box, from -1 to 1. |
| clipped | boolean | Clips the content to the box's frame. |
| colorInvert | boolean | Inverts every colour in the box. |
| containerRelativeFrame | {alignment?: FrameAlignment; axes: 'both' | 'horizontal' | 'vertical'; count?: number; span?: number; spacing?: number} | Sizes the box relative to the nearest container: split it into |
| contentTransition | {countsDown?: boolean; type: 'identity' | 'interpolate' | 'numericText' | 'opacity'} | How a content change animates when one is driven natively. |
| contrast | number | Scales the contrast: 1 leaves it, 0 is flat grey, above 1 sharpens it. |
| controlSize | 'extraLarge' | 'large' | 'mini' | 'regular' | 'small' | Cascades to every control in the subtree, the SwiftUI environment way. |
| fixedSize | 'both' | 'horizontal' | 'vertical' | Holds the box at its ideal size on these axes, whatever its parent proposes. |
| glass | GlassVariant | {cornerRadius?: number; interactive?: boolean; shape?: 'capsule' | 'circle' | 'containerRelativeShape' | 'ellipse' | 'rectangle' | 'roundedRectangle'; tint?: ColorToken; variant: GlassVariant} | Liquid Glass: a bare variant, or the full config. |
| grayscale | number | Drains the colour, from 0 (none) to 1 (fully grey). |
| hidden | boolean | Invisible, but still taking up its layout space — unlike not rendering the box at all. |
| hueRotation | number | In degrees. |
| idealH | number | The height SwiftUI prefers when nothing constrains it — what |
| idealW | number | The width SwiftUI prefers when nothing constrains it — what |
| ignoreSafeArea | boolean | {edges?: 'all' | 'bottom' | 'horizontal' | 'leading' | 'top' | 'trailing' | 'vertical'; regions?: 'all' | 'container' | 'keyboard'} |
|
| layoutPriority | number | Who gets space first when the parent runs short — not a proportional flex weight. |
| luminanceToAlpha | boolean | Turns brightness into transparency: dark becomes clear, light stays opaque. |
| modifiers | IosModifier[] | Escape hatch, appended last. Pass a stable array — a fresh one every render defeats the memo. |
| privacySensitive | boolean | Redacts only when an ancestor applies the |
| redacted | 'invalidated' | 'placeholder' | 'privacy' | Redacts this subtree — |
| saturation | number | Scales the saturation: 1 leaves it, 0 is grey, above 1 intensifies it. |
| tint | ColorToken | Cascades to the controls and icons in the subtree. |
| unredacted | boolean | Exempts this subtree from an ancestor's |
Width over height, fitted within the proposed size. The escape hatch covers fill.
Brightens (positive) or darkens (negative) the box, from -1 to 1.
Clips the content to the box's frame.
Inverts every colour in the box.
Sizes the box relative to the nearest container: split it into count columns spacing apart and span span of them — SwiftUI's stand-in for a fractional width, which its frame lacks.
How a content change animates when one is driven natively. countsDown is numericText's.
Scales the contrast: 1 leaves it, 0 is flat grey, above 1 sharpens it.
Cascades to every control in the subtree, the SwiftUI environment way.
Holds the box at its ideal size on these axes, whatever its parent proposes.
Liquid Glass: a bare variant, or the full config. tint takes a theme token, and without a shape the glass traces the box's own borderRadius, the way strokes and shadows do.
Drains the colour, from 0 (none) to 1 (fully grey).
Invisible, but still taking up its layout space — unlike not rendering the box at all.
In degrees.
The height SwiftUI prefers when nothing constrains it — what fixedSize resolves to.
The width SwiftUI prefers when nothing constrains it — what fixedSize resolves to.
true extends into every safe area; the config narrows which regions and edges.
Who gets space first when the parent runs short — not a proportional flex weight.
Turns brightness into transparency: dark becomes clear, light stays opaque.
Escape hatch, appended last. Pass a stable array — a fresh one every render defeats the memo.
Redacts only when an ancestor applies the privacy reason — inert on its own.
Redacts this subtree — placeholder is SwiftUI's built-in skeleton. One reason here; combining several is escape-hatch territory.
Scales the saturation: 1 leaves it, 0 is grey, above 1 intensifies it.
Exempts this subtree from an ancestor's redacted.
android
| Prop | Type | Description |
|---|---|---|
| align | 'bottom' | 'bottomCenter' | 'bottomEnd' | 'bottomStart' | 'center' | 'centerEnd' | 'centerHorizontally' | 'centerStart' | 'centerVertically' | 'end' | 'start' | 'top' | 'topCenter' | 'topEnd' | 'topStart' | This box's own placement inside a |
| animateContentSize | boolean | |
| fillMaxHeight | number | Fraction of the parent's height (0–1). The whole of it is |
| fillMaxWidth | number | Fraction of the parent's width (0–1). The whole of it is |
| imePadding | boolean | |
| matchParentSize | boolean | |
| modifiers | AndroidModifier[] | Escape hatch, appended last. Pass a stable array — a fresh one every render defeats the memo. |
| weight | number | Proportional share of the leftover space in a Row or Column, the way |
| wrapContentHeight | 'bottom' | 'centerVertically' | 'top' | boolean | |
| wrapContentWidth | 'centerHorizontally' | 'end' | 'start' | boolean |
This box's own placement inside a ZStack parent — Compose's per-child Modifier.align.
Fraction of the parent's height (0–1). The whole of it is tall, which works on iOS too.
Fraction of the parent's width (0–1). The whole of it is wide, which works on iOS too.
Escape hatch, appended last. Pass a stable array — a fresh one every render defeats the memo.
Proportional share of the leftover space in a Row or Column, the way flexGrow behaves.
React Native sx
The React Native tier — Box, Pressable, GlassView, the Rn* views — takes everything above except the buckets, plus the layout only a React Native style has. Lengths there may also be percentages: w="50%".
Flex
| Prop | Type | Description |
|---|---|---|
| align | ViewStyle['alignItems'] | |
| alignContent | ViewStyle['alignContent'] | |
| alignSelf | ViewStyle['alignSelf'] | |
| basis | SxDimension | |
| columnGap | SpaceSize | |
| flex | number | |
| flexDirection | ViewStyle['flexDirection'] | |
| gap | SpaceSize | |
| grow | number | |
| justify | ViewStyle['justifyContent'] | |
| rowGap | SpaceSize | |
| shrink | number | |
| wrap | ViewStyle['flexWrap'] |
Margins
Position and overflow
| Prop | Type | Description |
|---|---|---|
| bottom | SxDimension | |
| display | ViewStyle['display'] | |
| left | SxDimension | |
| overflow | ViewStyle['overflow'] | |
| position | ViewStyle['position'] | |
| right | SxDimension | |
| top | SxDimension |
Sizing
| Prop | Type | Description |
|---|---|---|
| aspectRatio | number | |
| maxH | SxDimension | |
| maxW | SxDimension |
Borders and corners
| Prop | Type | Description |
|---|---|---|
| borderBottomWidth | number | |
| borderLeftWidth | number | |
| borderRightWidth | number | |
| borderStyle | ViewStyle['borderStyle'] | |
| borderTopWidth | number |
Responsive values
Layout keys take a value per breakpoint as well as a single one — mobile first, from base up. Colours, radii, shadows and the platform buckets stay scalar.
<Box flexDirection={{base: 'column', md: 'row'}} gap="s16" p={{base: 's16', md: 's32'}} />| Breakpoint | From |
|---|---|
base | 0 |
sm | 576 |
md | 768 |
lg | 992 |
xl | 1200 |
Dimmed colours
A colour token takes an opacity token after a colon: bg="fill.accent.base:o50". It multiplies whatever the colour already is. Tokens iOS resolves by name (UIKit semantic colours) have no alpha to multiply, so only the ones kara owns the value of can be dimmed — the types say which.
Your own components
sx(Component) gives any component that takes style the same props, memoised. useSx and useSplitSx are the hooks underneath, for a component that owns its style prop.
import {sx} from '@hzblj/kara-ui'
import {Image as ExpoImage} from 'expo-image'
/** Any component that takes `style` — wrapped once, at module scope. */
export const Photo = sx(ExpoImage)
<Photo borderRadius="large" h={220} source={photo} w="100%" />