Files
2026-09-04 09:04:51 -07:00

574 lines
18 KiB
Plaintext

---
title: Styling
description: Gui accepts a superset of React Native styles
---
Gui supports a superset of the React Native style properties on to any
styled component like the base
[View and Text components](/docs/core/view-and-text), or through the
[`styled()` function](/docs/core/styled) as the second argument.
Here's how that looks in practice:
```tsx
import { View, styled } from '@hanzogui/core'
const StyledView = styled(View, {
padding: 10,
})
const MyView = () => (
<StyledView
backgroundColor="red"
hoverStyle={{
backgroundColor: 'green',
}}
/>
)
```
The types for the full set of styles accepted by styled, View and Text are
exported as `ViewStyle` and `TextStyle`.
For the full base styles, see the React Native docs (version 2 targets React
Native 0.82):
- [React Native View style props](https://reactnative.dev/docs/view-style-props)
- [React Native Text style props](https://reactnative.dev/docs/text-style-props)
The full Gui typed style props can be simplified to something like this,
except the values can also accept one of your design tokens:
```tsx
import { ViewStyle as RNViewStyle } from 'react-native'
type BaseViewStyle = RNViewStyle & FlatTransformStyles & CrossPlatformStyles & WebOnlyStyles
// Cross-platform styles:
type CrossPlatformStyles = {
boxShadow?: BoxShadowValue // string, object, or array
filter?: FilterValue // brightness, opacity cross-platform; blur, contrast, etc. Android 12+
backgroundImage?: string // linear-gradient, radial-gradient; supports $tokens
cursor?: Properties['cursor'] // web + iOS 17+ (trackpad/stylus/gaze)
mixBlendMode?: 'normal' | 'multiply' | 'screen' | 'overlay' | ... // blend modes
isolation?: 'auto' | 'isolate'
boxSizing?: 'border-box' | 'content-box'
display?: 'flex' | 'grid' | 'none' | 'contents' | ...
position?: 'absolute' | 'relative' | 'fixed' | 'static' | 'sticky' // 'fixed' converts to 'absolute' on native
border?: BorderValue // shorthand: "1px solid $borderColor"; expands on native
outline?: OutlineValue // shorthand: "2px solid $outlineColor"; expands on native
outlineColor?: ColorValue
outlineOffset?: SpaceValue
outlineStyle?: 'solid' | 'dotted' | 'dashed'
outlineWidth?: SpaceValue
}
// these are accepted but only render on web:
type WebOnlyStyles = {
contain?: Properties['contain']
touchAction?: Properties['touchAction']
backdropFilter?: Properties['backdropFilter']
backgroundOrigin: Properties['backgroundOrigin'],
backgroundPosition: Properties['backgroundPosition'],
backgroundRepeat: Properties['backgroundRepeat'],
backgroundSize: Properties['backgroundSize']
backgroundColor: Properties['backgroundColor']
backgroundClip: Properties['backgroundClip']
backgroundBlendMode: Properties['backgroundBlendMode']
backgroundAttachment: Properties['backgroundAttachment']
background: Properties['background']
clipPath: Properties['clipPath'],
caretColor: Properties['caretColor']
transformStyle: Properties['transformStyle'],
mask: Properties['mask'],
maskImage: Properties['maskImage'],
textEmphasis: Properties['textEmphasis'],
borderImage: Properties['borderImage'],
float: Properties['float']
content: Properties['content']
overflowBlock: Properties['overflowBlock']
overflowInline: Properties['overflowInline']
maskBorder: Properties['maskBorder']
maskBorderMode: Properties['maskBorderMode']
maskBorderOutset: Properties['maskBorderOutset']
maskBorderRepeat: Properties['maskBorderRepeat']
maskBorderSlice: Properties['maskBorderSlice']
maskBorderSource: Properties['maskBorderSource']
maskBorderWidth: Properties['maskBorderWidth']
maskClip: Properties['maskClip']
maskComposite: Properties['maskComposite']
maskMode: Properties['maskMode']
maskOrigin: Properties['maskOrigin']
maskPosition: Properties['maskPosition']
maskRepeat: Properties['maskRepeat']
maskSize: Properties['maskSize']
maskType: Properties['maskType']
}
// these turn into the equivalent transform style props:
type FlatTransformStyles = {
x?: number
y?: number
perspective?: number
scale?: number
scaleX?: number
scaleY?: number
skewX?: string
skewY?: string
matrix?: number[]
rotate?: string
rotateY?: string
rotateX?: string
rotateZ?: string
}
// add the pseudo and enter/exit style states
type WithStates = BaseViewStyle & {
hoverStyle?: BaseViewStyle
pressStyle?: BaseViewStyle
focusStyle?: BaseViewStyle
focusVisibleStyle?: BaseViewStyle
focusWithinStyle?: BaseViewStyle
disabledStyle?: BaseViewStyle
enterStyle?: BaseViewStyle
exitStyle?: BaseViewStyle
}
// final View style props
type ViewStyle = WithStates & {
// add media queries
$sm?: WithStates
// add group queries
$group-hover?: WithStates
$group-focus?: WithStates
$group-press?: WithStates
// add group + container queries
$group-sm-hover?: WithStates
$group-sm-focus?: WithStates
$group-sm-press?: WithStates
// add named group queries
$group-tabs?: WithStates
$group-tabs-hover?: WithStates
$group-tabs-focus?: WithStates
$group-tabs-press?: WithStates
// add named group + container queries
$group-tabs-sm?: WithStates
$group-tabs-sm-hover?: WithStates
$group-tabs-sm-focus?: WithStates
$group-tabs-sm-press?: WithStates
// add theme queries
$theme-light?: WithStates
$theme-dark?: WithStates
// add platform queries
$platform-native?: WithStates
$platform-ios?: WithStates
$platform-android?: WithStates
$platform-web?: WithStates
}
// Text style starts with this base but builds up the same:
type TextStyleBase = BaseViewStyle & {
color?: string,
fontFamily?: string,
fontSize?: string,
fontStyle?: string,
fontWeight?: string,
letterSpacing?: string,
lineHeight?: string,
textAlign?: string,
textDecorationColor?: string,
textDecorationLine?: string,
textDecorationStyle?: string,
textShadowColor?: string,
textShadowOffset?: string,
textShadowRadius?: string,
textTransform?: string,
}
```
## Pseudo States
Gui supports several style states that apply based on interaction or focus:
- **`hoverStyle`** - Styles applied when hovering over the element (web only, maps to CSS `:hover`)
- **`pressStyle`** - Styles applied while the element is being pressed
- **`focusStyle`** - Styles applied when the element has focus (maps to CSS `:focus`)
- **`focusVisibleStyle`** - Styles applied when the element has keyboard focus (maps to CSS `:focus-visible`). Use this for keyboard navigation indicators.
- **`focusWithinStyle`** - Styles applied when any child element has focus (maps to CSS `:focus-within`). Useful for styling containers when an input inside them is focused.
- **`disabledStyle`** - Styles applied when `disabled={true}` is set
```tsx
<View
backgroundColor="$background"
hoverStyle={{ backgroundColor: '$backgroundHover' }}
pressStyle={{ backgroundColor: '$backgroundPress', scale: 0.98 }}
focusStyle={{ outlineColor: '$blue10', outlineWidth: 2, outlineStyle: 'solid' }}
focusVisibleStyle={{ outlineColor: '$blue10', outlineWidth: 2 }}
focusWithinStyle={{ borderColor: '$blue10' }}
disabledStyle={{ opacity: 0.5 }}
/>
```
### Enter and Exit Styles
For mount/unmount animations, use:
- **`enterStyle`** - Initial styles when the component mounts (animates _from_ these values)
- **`exitStyle`** - Final styles when the component unmounts (animates _to_ these values)
See [Animations](/docs/core/animations) for more details on enter/exit animations.
## rem Units
Gui supports `rem` units cross-platform. On web, browsers handle rem natively. On native, Gui converts rem values to pixels using `PixelRatio.getFontScale()` to respect the user's font size preferences.
```tsx
// works on both web and native
<View padding="1rem" />
<Text fontSize="1.5rem" />
```
You can configure the base font size (default 16) in your config:
```tsx
createGui({
settings: {
remBaseFontSize: 16, // default
},
})
```
On native, `1rem` with a base of 16 equals `16 * PixelRatio.getFontScale()` pixels, scaling with the user's accessibility settings.
## CSS Shorthand with Variables
Gui supports using `$variables` directly inside CSS shorthand string values.
This is particularly useful for `boxShadow` and `backgroundImage` where you want
to reference theme colors:
```tsx
// boxShadow with variables
<View boxShadow="0 0 10px $shadowColor" />
<View boxShadow="0 0 5px $shadowColor, 0 0 15px $color" />
// backgroundImage with linear-gradient (cross-platform, RN 0.76+)
<View backgroundImage="linear-gradient(to bottom, $background, $color)" />
<View backgroundImage="linear-gradient(45deg, $black 0%, $white 50%, $black 100%)" />
// filter with variables
<View filter="blur($2)" />
// On web: resolves to CSS custom properties
// boxShadow: "0 0 10px var(--t-shadow-shadowColor)"
// backgroundImage: "linear-gradient(to bottom, var(--background), var(--color))"
// On native: resolves to raw values
// boxShadow: "0 0 10px rgba(0,0,0,0.2)"
// backgroundImage: "linear-gradient(to bottom, #fff, #000)"
```
Supported shorthand properties with variable resolution:
- `boxShadow` - Works on both web and native
- `backgroundImage` - Works on both web and native (see below)
- `filter` - Works on both web and native
- `border` - Works on both web and native (expands to borderWidth/Style/Color on native)
- `outline` - Works on both web and native (expands to outlineWidth/Style/Color on native)
- `borderTop`, `borderRight`, `borderBottom`, `borderLeft` - Web only
- `background` - Web only
### border
The `border` shorthand now works cross-platform:
```tsx
// Works on both web and native
<View border="1px solid $borderColor" />
<View border="2px dashed red" />
<View border="none" />
```
On web, the CSS shorthand is passed through directly. On native, it expands to individual properties:
- `borderWidth` → `borderTopWidth`, `borderRightWidth`, `borderBottomWidth`, `borderLeftWidth`
- `borderStyle` → `borderStyle`
- `borderColor` → `borderTopColor`, `borderRightColor`, `borderBottomColor`, `borderLeftColor`
Note: On native, only a single border (all sides) is supported. For per-side borders on native, use individual props.
### outline
The `outline` shorthand works cross-platform, similar to `border`:
```tsx
// Works on both web and native
<View outline="2px solid $outlineColor" />
<View outline="1px dashed red" />
<View outline="none" />
```
On web, the CSS shorthand is passed through directly. On native, it expands to `outlineWidth`, `outlineStyle`, and `outlineColor`.
### backgroundImage
Supports `linear-gradient()` and `radial-gradient()` cross-platform. Does not
support `url()` on native - use `<ImageBackground>` for images.
```tsx
<View backgroundImage="linear-gradient(to bottom, $background, $color)" />
<View backgroundImage="radial-gradient(circle, $white, $black)" />
```
### textShadow
Text shadows support CSS shorthand with token resolution, just like `boxShadow`:
```tsx
import { Text } from '@hanzo/gui'
// CSS shorthand with tokens
<Text textShadow="2px 2px 4px $shadowColor">
Shadowed Text
</Text>
// multiple shadows (web only)
<Text textShadow="1px 1px 2px $shadowColor, 0 0 8px $color">
Glowing Text
</Text>
```
You can also use the individual React Native props:
```tsx
<Text
textShadowColor="$shadowColor"
textShadowOffset={{ width: 2, height: 2 }}
textShadowRadius={4}
>
Shadowed Text
</Text>
```
On native, only a single text shadow is supported (React Native limitation).
## Parent based styling
Gui has a variety of ways to style a child based on the "parent", a parent
being one of: platform, screen size, theme, or group. All of these styles use
the same pattern, they use a `$` prefix for their styles, and they nest styles
as a sub-object.
For example you can target `$theme-light`, `$platform-ios`, or `$group-header`.
For screen size, which we call media queries, they have no prefix. Instead you
define media queries on `createGui`. For example, if you define a media
query named `large`, then `$large` is the prop name.
These parent style props accept all the Gui style props in their value
object.
#### Media query
Based on whatever media queries you define in `createGui`, you can now use
any of them to apply styling on native and web using the `$` prefix.
If you defined your media query like:
```tsx
createGui({
media: {
sm: { maxWidth: 800 },
},
})
```
Then you can use it like:
```tsx
<Text color="red" $sm={{ color: 'blue' }} />
```
### Theme
Theme style props let you style a child based on a parent theme. At the moment,
they only can target your top level themes, so if you have `light`, and
`light_subtle` themes, then only `light` can be targeted.
Use them like so:
```tsx
<Text $theme-dark={{ color: 'white' }} />
```
### Platform
Platform style props let you style a child based on the platform the app is
running on. This can be one of `ios`, `android`, `web`, or `native` (iOS and
Android).
Use it like so:
```tsx
<Text $platform-ios={{ color: 'white' }} />
```
### Group
Groups are a new feature in beta that lets you define a named group, and then
style children based whether they are inside a parent that is given that group
name.
A short example:
```tsx
<View group="header">
<Text $group-header={{ color: 'white' }} />
</View>
```
This will make the Text turn white, as it's inside a parent item with `group`
set to the matching `header` value.
Group styles also allow for targeting the parent pseudo state:
```tsx
<View group>
<Text $group-hover={{ color: 'white' }} />
</View>
```
Now only when the parent View is hovered the Text will turn white. The allowed
pseudo modifiers are `hover` (web only), `press`, and `focus`.
For more advanced use cases you can use named groups:
```tsx
<View group="card">
<Text>Outer</Text>
<View group>
<Text $group-card-hover={{ color: 'blue' }}>Inner</Text>
<Text $group-hover={{ color: 'green' }}>Sibling</Text>
</View>
</View>
```
Now the `Inner` Text will turn blue when the `card` group is hovered, and the
`Sibling` Text will turn green when its parent is hovered.
To make this typed, you need to set `TypeOverride` alongside the same area you
set up your Gui types:
```tsx
declare module 'hanzogui' {
interface GuiCustomConfig extends AppConfig {}
// if you want types for named group styling props (e.g. $group-card-hover),
// define your group names here:
interface TypeOverride {
groupNames(): 'card' | 'header' | 'sidebar'
}
}
```
#### Group Container
The final feature of group styles is the ability to style a child only when the
parent is of a certain size. On the web these are known as "container queries",
which is what Gui outputs as CSS under the hood. They look like this:
```tsx
<View group>
<Text $group-sm={{ color: 'white' }} $group-sm-hover={{ color: 'green' }} />
</View>
```
Now the Text will be white, but only when the View matches the media query `sm`.
This uses the same media query breakpoints you defined in
`createGui({ media })`. You can see it also works with pseudo styles!
For more advanced use cases, you can use named groups with container queries:
```tsx
<View group="card">
<View group>
<Text $group-card-sm={{ color: 'white' }} $group-card-sm-hover={{ color: 'green' }} />
<Text $group-sm={{ color: 'white' }} $group-sm-hover={{ color: 'green' }} />
</View>
</View>
```
Now the first Text will be white when the `card` parent matches `sm`, and the
second Text will be white when no named parent matches `sm`.
_A note on group containers and native_
On Native, we don't have access to the layout of a React component as it first
renders. Only once we get the dimensions from the `onLayout` callback after the
first render are we able to apply group container styles.
Because of this, we've done two things.
First, there's a new property `untilMeasured`:
```tsx
<View group untilMeasured="hide">
<Text
$group-sm={{
color: 'white',
}}
/>
</View>
```
This takes two options, `show` or `hide`. If unset it defaults to `show`, which
means it will render before it measures. With `hide` set, the container will be
set to `opacity` 0 until it finishes measuring.
Alternatively, if you know the dimensions your container will be up-front, you
can set width and height on the container. When either of these are set, the
children will attempt to use these values on first render and if they satisfy
the media query, you'll avoid the need for a double render altogether.
## Order is important
It's important to note that the order of style properties is significant. This
is really important for two reasons:
1. You want to control which styles are overridden.
2. You have a variant that expands into multiple style properties, and you need
to control it.
Let's see how it lets us control overriding styles:
```tsx
import { View, ViewProps } from '@hanzogui/core'
export default (props: ViewProps) => <View background="red" {...props} width={200} />
```
In this case we set a default `background` to red, but it can be overridden by
props. But we set `width` _after_ the prop spread, so width is _always_ going to
be set to 200.
It also is necessary for variants to make sense. Say we have a variant `huge`
that sets `scale` to 2 and `borderRadius` to 100:
```tsx
// this will be scale = 3
export default (props: ViewProps) => <MyView huge scale={3} />
// this will be scale = 2
export default (props: ViewProps) => <MyView scale={3} huge />
```
If order wasn't important, how would you expect these two different usages to
work? You'd have to make order important _somewhere_. If you do it in the
`styled()` helper somewhere, you end up having no flexibility and would end up
with boilerplate. Making the prop order important gives us maximum
expressiveness and is easy to understand.