Type-safe and zero-runtime UI styling, right in the markup.
Vicinage lets you write strongly-typed CSS objects directly on your markup. At build time, it preprocesses them into StyleX API calls, which StyleX then extracts into zero-runtime atomic CSS, with no style block naming required.
Table of Contents
Install the packages:
npm install vicinage @stylexjs/stylex
npm install --save-dev @vicinage/unplugin @stylexjs/unpluginAdd the plugin to your bundler configuration right before the StyleX plugin.
import { defineConfig } from 'vite'
import vicinage from '@vicinage/unplugin'
import stylex from '@stylexjs/unplugin'
export default defineConfig({
plugins: [
vicinage.vite(),
stylex.vite(),
// ...other plugins
],
})-
Design tokens from SolarWind CSS
-
Chrome Extension StyleX DevTools
-
VS Code extension Explicit Folding
Recommended settings
{ "[javascriptreact][typescriptreact]": { "editor.defaultFoldingRangeProvider": "zokugun.explicit-folding", "explicitFolding.rules": [ { "beginRegex": "^\\s*<[a-zA-Z][a-zA-Z0-9-]*", "endRegex": "(?<!=)>$", "autoFold": true, "foldLastLine": true } ] } }
Apply styles directly to HTML elements.
import { apply } from 'vicinage'
function App() {
return (
<div
{...apply({
color: 'green',
backgroundColor: 'black',
})}
>
hello, world
</div>
)
}Pass styles with the sheet() function to components that accept StyleXStyles.
import { apply, sheet } from 'vicinage'
import type { StyleXStyles } from '@stylexjs/stylex'
function Feed() {
return (
<Post
style={sheet({
color: 'blue',
})}
/>
)
}
function Post({ style }: { style?: StyleXStyles }) {
return (
<div
{...apply(
{
color: 'black',
},
style,
)}
>
Lorem ipsum
</div>
)
}import { apply } from 'vicinage'
function Hero() {
return (
<h1
{...apply({
fontSize: {
default: '1.5rem',
'@media (min-width: 768px)': '2.25rem',
},
})}
>
Welcome back
</h1>
)
}import { apply } from 'vicinage'
function BillingLink() {
return (
<a
{...apply({
color: {
default: 'blue',
':visited': 'purple',
},
})}
href="/billing/"
>
Open billing
</a>
)
}import { apply } from 'vicinage'
function SearchInput() {
return (
<input
placeholder="Search"
{...apply({
color: 'black',
'::placeholder': {
color: 'gray',
},
})}
/>
)
}Custom properties for app-level theming and inline overrides:
/* main.css */
:root {
--sidebar-width: 240px;
--color-surface: lightblue;
}import { apply } from 'vicinage'
function Sidebar() {
return (
<nav
{...apply({
'--sidebar-width': '320px',
width: 'var(--sidebar-width)',
backgroundColor: 'var(--color-surface, blue)',
})}
>
Navigation
</nav>
)
}StyleX variables for shared design tokens:
// tokens.stylex.ts
import * as stylex from '@stylexjs/stylex'
export const color = stylex.defineVars({
primary: 'blue',
})import { apply } from 'vicinage'
import { color } from './tokens.stylex'
function PrimaryButton({ label }: { label: string }) {
return (
<button
{...apply({
backgroundColor: color.primary,
})}
>
{label}
</button>
)
}import { apply } from 'vicinage'
import * as stylex from '@stylexjs/stylex'
const typography = stylex.create({
caption: {
fontSize: '0.75rem',
lineHeight: '1rem',
fontStyle: 'italic',
},
})
function Timestamp() {
return (
<time
{...apply(
{
color: 'black',
},
typography.caption,
)}
>
2 minutes ago
</time>
)
}import { apply } from 'vicinage'
function SaveButton({ isEnabled }: { isEnabled: boolean }) {
return (
<button
{...apply({
fontWeight: isEnabled && 'bold',
backgroundColor: isEnabled ? 'blue' : 'gray',
})}
>
Save changes
</button>
)
}Use function to define runtime dynamic values.
import { apply } from 'vicinage'
function ProgressBar({ percentage }: { percentage: number }) {
return (
<div
{...apply({
width: () => `${percentage}%`,
height: '16px',
backgroundColor: 'blue',
})}
/>
)
}Dynamic values can also be deeply nested.
import { apply } from 'vicinage'
function Swatch({ hue }: { hue: number }) {
return (
<div
{...apply({
backgroundColor: {
default: () => `hsl(${hue}, 60%, 50%)`,
':hover': () => `hsl(${hue}, 80%, 40%)`,
},
})}
/>
)
}NOTE: The function body must be an expression statement. You cannot use a function body with block statement.