|
1 | 1 | import {Layout} from '../../src/Layout'; |
2 | 2 | import {InlineAlert, Heading, Content, Link} from '@react-spectrum/s2'; |
3 | | -import {S2Colors} from '../../src/S2Colors'; |
4 | | -import {S2Typography} from '../../src/S2Typography'; |
5 | 3 | import {S2StyleProperties} from '../../src/S2StyleProperties'; |
6 | 4 | import {S2FAQ} from '../../src/S2FAQ'; |
7 | 5 | export default Layout; |
8 | 6 |
|
9 | 7 | export const section = 'Guides'; |
10 | 8 | export const tags = ['style', 'macro', 'spectrum', 'custom']; |
11 | 9 | export const description = 'Styling in React Spectrum'; |
12 | | -export const relatedPages = [ |
13 | | - {title: 'Advanced', url: './styling/advanced.html'}, |
14 | | - {title: 'Reference Table', url: './styling/reference.html'} |
15 | | -]; |
16 | 10 |
|
17 | 11 | # Styling |
18 | 12 |
|
@@ -99,58 +93,214 @@ import {Button} from '@react-spectrum/s2'; |
99 | 93 | 'visibility' |
100 | 94 | ]} /> |
101 | 95 |
|
102 | | -## Values |
| 96 | +## Conditional styles |
103 | 97 |
|
104 | | -The `style` macro supports a constrained set of values per property that conform to Spectrum 2. This improves consistency and maintainability. See the [reference](./styling/reference.html) page for a full list of available style macro properties. |
| 98 | +Define conditional values as objects to handle media queries, UI states (hover/press), and variants. This keeps all values for a property together. |
| 99 | +Note how the example below uses a predefined [breakpoint](./reference.html#conditions) alongside a custom media query. |
105 | 100 |
|
106 | | -### Colors |
| 101 | +```tsx |
| 102 | +<div |
| 103 | + className={style({ |
| 104 | + padding: { |
| 105 | + default: 8, |
| 106 | + lg: 32, |
| 107 | + '@media (min-width: 2560px)': 64, |
| 108 | + } |
| 109 | + })} |
| 110 | +/> |
| 111 | +``` |
107 | 112 |
|
108 | | -All Spectrum 2 color tokens are available across color properties (e.g., `backgroundColor`, `color`, `borderColor`). |
| 113 | +Conditions are mutually exclusive and ordered. The macro uses CSS cascade layers so the last matching condition wins without specificity issues. |
109 | 114 |
|
110 | | -<S2Colors /> |
| 115 | +## Runtime conditions |
111 | 116 |
|
112 | | -### Spacing |
| 117 | +When runtime conditions are detected (e.g., variants, UI states), the macro returns a function to resolve styles at runtime. |
113 | 118 |
|
114 | | -Spacing props like `margin` and `padding` accept values on a **4px grid**. These are specified in `px` and get converted to `rem`. In addition to numbers, these named options are available: |
| 119 | +```tsx |
| 120 | +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; |
115 | 121 |
|
116 | | -- `edge-to-text` – default spacing between the edge of a control and its text. Relative to control height. |
117 | | -- `pill` – default spacing between the edge of a pill-shaped control and its text. Relative to control height. |
118 | | -- `text-to-control` – default spacing between text and a control (e.g., label and input). Scales with font size. |
119 | | -- `text-to-visual` – default spacing between text and a visual element (e.g., icon). Scales with font size. |
| 122 | +const styles = style({ |
| 123 | + backgroundColor: { |
| 124 | + variant: { |
| 125 | + primary: 'accent', |
| 126 | + secondary: 'neutral' |
| 127 | + } |
| 128 | + } |
| 129 | +}); |
120 | 130 |
|
121 | | -### Sizing |
| 131 | +function MyComponent({variant}: {variant: 'primary' | 'secondary'}) { |
| 132 | + return <div className={styles({variant})} /> |
| 133 | +} |
| 134 | +``` |
122 | 135 |
|
123 | | -Size props like `width` and `height` accept arbitrary pixel values. Values are converted to `rem` and multiplied by 1.25x on touch devices to increase hit targets. |
| 136 | +Boolean conditions starting with `is` or `allows` can be used directly without nesting: |
124 | 137 |
|
125 | | -### Typography |
| 138 | +```tsx |
| 139 | +const styles = style({ |
| 140 | + backgroundColor: { |
| 141 | + default: 'gray-100', |
| 142 | + isSelected: 'gray-900', |
| 143 | + allowsRemoving: 'gray-400' |
| 144 | + } |
| 145 | +}); |
| 146 | + |
| 147 | +<div className={styles({isSelected: true})} /> |
| 148 | +``` |
126 | 149 |
|
127 | | -Spectrum 2 typography is applied via the `font` shorthand, which sets `fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, and `color`. You can override any of these individually. |
128 | | -Note that you should always specify the `font` at the element level, setting it globally is insufficient since this will often differ per component. |
| 150 | +Runtime conditions work well with render props in React Aria Components. If you inline styles, you’ll get autocomplete for available conditions. |
129 | 151 |
|
130 | 152 | ```tsx |
131 | | -<main> |
132 | | - <h1 className={style({font: 'heading-xl'})}>Heading</h1> |
133 | | - <p className={style({font: 'body'})}>Body</p> |
134 | | - <ul className={style({font: 'body-sm', fontWeight: 'bold'})}> |
135 | | - <li>List item</li> |
136 | | - </ul> |
137 | | -</main> |
| 153 | +import {Checkbox} from 'react-aria-components'; |
| 154 | +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; |
| 155 | + |
| 156 | +<Checkbox |
| 157 | + className={style({ |
| 158 | + backgroundColor: { |
| 159 | + default: 'gray-100', |
| 160 | + isHovered: 'gray-200', |
| 161 | + isSelected: 'gray-900' |
| 162 | + } |
| 163 | + })} |
| 164 | +/> |
138 | 165 | ``` |
139 | 166 |
|
140 | | -Type scales include: UI, Body, Heading, Title, Detail, and Code. Each scale has a default and additional t-shirt sizes (e.g., `ui-sm`, `heading-2xl`, `code-xl`). |
| 167 | +### Nesting conditions |
141 | 168 |
|
142 | | -<S2Typography /> |
| 169 | +Nest conditions to apply styles when multiple conditions are true. Conditions at the same level are mutually exclusive; order determines precedence. |
143 | 170 |
|
144 | | -<InlineAlert variant="notice"> |
145 | | - <Heading>Important Note</Heading> |
146 | | - <Content> |
147 | | - Only use `<Heading>` and `<Text>` inside Spectrum components with predefined styles (e.g., `<Dialog>`, `<MenuItem>`). They are unstyled by default and should not be used standalone. Use HTML elements with the `style` macro instead. |
148 | | - </Content> |
149 | | -</InlineAlert> |
| 171 | +```tsx |
| 172 | +const styles = style({ |
| 173 | + backgroundColor: { |
| 174 | + default: 'gray-25', |
| 175 | + isSelected: { |
| 176 | + default: 'neutral', |
| 177 | + isEmphasized: 'accent', |
| 178 | + forcedColors: 'Highlight', |
| 179 | + isDisabled: { |
| 180 | + default: 'gray-400', |
| 181 | + forcedColors: 'GrayText' |
| 182 | + } |
| 183 | + } |
| 184 | + } |
| 185 | +}); |
| 186 | + |
| 187 | +<div className={styles({isSelected, isEmphasized, isDisabled})} /> |
| 188 | +``` |
| 189 | + |
| 190 | +## Reusing styles |
| 191 | + |
| 192 | +Extract common styles into constants and spread them into `style` calls. These must be in the same file or imported from another file as a macro. |
| 193 | + |
| 194 | +```tsx |
| 195 | +// style-utils.ts |
| 196 | +export const bannerBackground = () => 'blue-1000' as const; |
| 197 | + |
| 198 | +// component.tsx |
| 199 | +import {bannerBackground} from './style-utils' with {type: 'macro'}; |
| 200 | +const horizontalStack = { |
| 201 | + display: 'flex', |
| 202 | + alignItems: 'center', |
| 203 | + columnGap: 8 |
| 204 | +} as const; |
| 205 | + |
| 206 | +const styles = style({ |
| 207 | + ...horizontalStack, |
| 208 | + backgroundColor: bannerBackground(), |
| 209 | + columnGap: 4 |
| 210 | +}); |
| 211 | +``` |
| 212 | + |
| 213 | +Create custom utilities by defining your own macros. |
| 214 | + |
| 215 | +```ts |
| 216 | +// style-utils.ts |
| 217 | +export function horizontalStack(gap: number) { |
| 218 | + return { |
| 219 | + display: 'flex', |
| 220 | + alignItems: 'center', |
| 221 | + columnGap: gap |
| 222 | + } as const; |
| 223 | +} |
| 224 | +``` |
| 225 | + |
| 226 | +Usage: |
| 227 | + |
| 228 | +```tsx |
| 229 | +// component.tsx |
| 230 | +import {horizontalStack} from './style-utils' with {type: 'macro'}; |
| 231 | +import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; |
| 232 | + |
| 233 | +const styles = style({ |
| 234 | + ...horizontalStack(4), |
| 235 | + backgroundColor: 'base' |
| 236 | +}); |
| 237 | +``` |
| 238 | + |
| 239 | +### Built-in utilities |
| 240 | + |
| 241 | +Use `focusRing()` to add the standard Spectrum focus ring. |
| 242 | + |
| 243 | +```tsx |
| 244 | +"use client"; |
| 245 | +import {style, focusRing} from '@react-spectrum/s2/style' with {type: 'macro'}; |
| 246 | +import {Button} from '@react-spectrum/s2'; |
| 247 | + |
| 248 | +const buttonStyle = style({ |
| 249 | + ...focusRing(), |
| 250 | + // ...other styles |
| 251 | +}); |
| 252 | + |
| 253 | +<Button styles={buttonStyle}>Press me</Button> |
| 254 | +``` |
150 | 255 |
|
151 | | -### Breakpoints |
| 256 | +## Setting CSS variables |
| 257 | + |
| 258 | +CSS variables can be directly defined in a `style` macro, allowing child elements to then access them in their own styles. |
| 259 | +A `type` should be provided to specify the CSS property type the `value` represents. |
| 260 | + |
| 261 | +```tsx |
| 262 | +const parentStyle = style({ |
| 263 | + '--rowBackgroundColor': { |
| 264 | + type: 'backgroundColor', |
| 265 | + value: 'gray-400' |
| 266 | + } |
| 267 | +}); |
| 268 | + |
| 269 | +const childStyle = style({ |
| 270 | + backgroundColor: '--rowBackgroundColor' |
| 271 | +}); |
| 272 | +``` |
| 273 | + |
| 274 | +## Creating custom components |
| 275 | + |
| 276 | +In-depth examples are coming soon! |
| 277 | + |
| 278 | +`mergeStyles` can be used to merge the style strings from multiple macros together, with the latter styles taking precedence similar to object spreading. |
| 279 | +This behavior can be leveraged to create a component API that allows an end user to only override specific styles conditionally. |
| 280 | + |
| 281 | +```tsx |
| 282 | +// User can override the component's background color ONLY if it isn't "quiet" |
| 283 | +const baselineStyles = style({backgroundColor: 'gray-100'}, ['backgroundColor']); |
| 284 | +const quietStyles = style({backgroundColor: 'transparent'}); |
| 285 | +const userStyles = style({backgroundColor: 'celery-600'}); |
| 286 | + |
| 287 | +function MyComponent({isQuiet, styles}: {isQuiet?: boolean, styles?: StyleString}) { |
| 288 | + let result = mergeStyles( |
| 289 | + baselineStyles(null, styles), |
| 290 | + isQuiet ? quietStyles : null |
| 291 | + ); |
| 292 | + |
| 293 | + return <div className={result}>My component</div> |
| 294 | +} |
| 295 | + |
| 296 | +// Displays quiet styles |
| 297 | +<MyComponent isQuiet styles={userStyles} /> |
| 298 | + |
| 299 | +// Displays user overrides |
| 300 | +<MyComponent styles={userStyles} /> |
| 301 | +``` |
152 | 302 |
|
153 | | -The style macro has several predefined [breakpoints](./styling/reference.html#conditions), but you can use arbitrary CSS media or container queries as [conditional values](./styling/advanced.html#conditional-styles) in your style macro as well. |
| 303 | +The `iconStyle` macro should be used when styling Icons, see the [docs](./Icons.html#iconstyle) for more information. |
154 | 304 |
|
155 | 305 | ## CSS optimization |
156 | 306 |
|
|
0 commit comments