vite-plugin-inline-style
v0.0.0
Published
Vite plugin to optimize JSX inline styling.
Maintainers
Readme
vite-plugin-inline-style
Vite plugin to optimize JSX inline styling.
Why, so?
Why?
We don't like TailwindCSS.
While Tailwind significantly boosts productivity, we disagree with having to study new names that simply map to what we already know well, just to reap the benefits Tailwind CSS offers. Even if they're just simple abbreviations.
So?
So we've decided to actively use inline styling. However, inline styling has several critical drawbacks:
- Inline styles are combined with the HTML file and cannot benefit from CDN caching or browser caching.
- Features like pseudo-classes and media queries cannot be utilized.
- Inline style objects in JSX are recreated each time, impacting performance.
That's why we created this plugin.
It analyzes inline styles at build time and separates each inline style into some separate CSS files. This preserves the familiarity of inline styles while leveraging caching benefits and solving the issue of object recreation. And it provides utility properties that enable features like pseudo-classes and media queries.
Installation
yarn add -D vite-plugin-inline-style// vite.config.js
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
import { inlineStyle } from 'vite-plugin-inline-style'
export default defineConfig({
plugins: [
react(), // or your favorite JSX plugin
inlineStyle(),
],
})Example
// Component.jsx
export function Component() {
return (
<div style={{
width: 200,
height: 200,
backgroundColor: '#F00',
color: '#FFF',
':hover': {
backgroundColor: '#0F0',
},
'@media (min-width: 768px)': {
width: '100%',
':hover': {
backgroundColor: '#00F',
},
}
}}>
Hello, world!
</div>
)
}<!-- Browser output -->
<style>
/** Minified and extracted to a separate file on production. **/
.s-xxxxxx {
width: 200px;
height: 200px;
background-color: #F00;
color: #FFF;
}
.s-xxxxxx:hover {
background-color: #0F0;
}
@media (min-width: 768px) {
.s-xxxxxx {
width: 100%;
}
.s-xxxxxx:hover {
background-color: #00F;
}
}
</style>
<div class="s-xxxxxx">Hello, world!</div>Details
Basic inline styling
This plugin determines extraction targets only when all keys and values are literals.
// ✅ Extractable
function Component () {
return (
<div style={{
width: 200,
}} />
)
}
// ⛔️ Not extractable (dynamic property)
function Component (props: { size: number }) {
return (
<div style={{
width: props.size,
}} />
)
}But literals that can be evaluated at build time, arithmetic operations on literals, and the spread of literal constant objects are included in the extraction target.
When using object spreads, it does not verify whether the object is immutable. If you are using TypeScript, it is highly
recommended to set the object as const.
// ✅ Extractable
function Component () {
return (
<div style={{
width: 200 + 100,
}} />
)
}
// ✅ Extractable
const style = {
height: 200,
} as const
function Component () {
return (
<div style={{
width: 200,
...style,
}} />
)
}
// ✅ Extractable
export const colors = {
red: '#F00',
} as const
import { colors } from './colors'
function Component () {
return (
<div style={{
backgroundColor: colors.red,
}} />
)
}
// 👀 Extractable, but cautious (no `as const`)
const invalidStyle = {
height: 200,
}
function Component () {
return (
<div style={{
...invalidStyle,
}} />
)
}Numeric units
When extracting numeric units, the plugin will convert some of them to px. Here are the properties that are converted:
export const pixelUnits = [
'top', 'right', 'bottom', 'left',
'gap', 'columnGap', 'rowGap',
'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight',
'margin', 'marginLeft', 'marginRight', 'marginTop', 'marginBottom',
'padding', 'paddingLeft', 'paddingRight', 'paddingTop', 'paddingBottom',
'borderWidth', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth',
'borderRadius',
'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius',
'fontSize', 'lineHeight', 'letterSpacing', 'wordSpacing', 'textIndent',
'textShadowOffsetX', 'textShadowOffsetY', 'textShadowBlur',
'boxShadowOffsetX', 'boxShadowOffsetY', 'boxShadowBlur',
]
// Example
function Component () {
return (
<div style={{
borderTopLeftRadius: 10, // Converted (border-top-left-radius: 10px)
flex: 1, // Not converted (flex: 1)
}} />
)
}Pseudo-classes and media queries
In modern web apps, you'll often use pseudo-classes and media queries. This plugin enables the use of pseudo-classes and media queries in inline styles.
function Component () {
return (
<div style={{
backgroundColor: '#F00',
':hover': {
backgroundColor: '#0F0',
},
}} />
)
}
function Component () {
return (
<div style={{
width: 640,
'@media (min-width: 768px)': {
width: '100%',
},
}} />
)
}Media queries cannot be declared inside pseudo-classes or nested declarations.
// ✅ Allowed
function Component () {
return (
<div style={{
'@media (min-width: 768px)': {
':hover': {
backgroundColor: '#00F',
},
},
}} />
)
}
// ⛔️ Not allowed (nested media queries)
function Component () {
return (
<div style={{
'@media (min-width: 768px)': {
'@media (min-width: 1024px)': {
backgroundColor: '#00F',
},
},
}}/>
)
}
// ⛔️ Not allowed (media query inside pseudo-class)
function Component () {
return (
<div style={{
':hover': {
'@media (min-width: 1024px)': {
backgroundColor: '#00F',
},
},
}}/>
)
}Keyframes
Keyframes have a different nature from media queries. They must be declared in the global scope and applied to specific
elements, but their names must be set as CSS property values. Therefore, this plugin allows objects to be used for the
animation properties.
The fields of the animation object correspond to each animation-* property by default. Among these, the name field
is assigned an automatically generated name based on the keyframe object passed as its value.
Additionally, we added a field named keyframes as an alias for the name field to provide a more intuitive
representation. Either can be used, but if both are provided, the plugin prioritizes keyframes.
function Component () {
return (
<div style={{
animation: {
duration: '1s',
keyframes: {
from: {
opacity: 0,
},
to: {
opacity: 1,
},
},
}
}} />
)
}
// Same as above, but with all animation properties set explicitly
function Component () {
return (
<div style={{
animation: {
duration: '1s',
keyframes: {
from: {
opacity: 0,
},
to: {
opacity: 1,
},
},
// Default values for other properties
composition: 'replace',
delay: '0s',
direction: 'normal',
fillMode: 'none',
iterationCount: 1,
playState: 'running',
range: 'normal',
rangeStart: 'normal',
rangeEnd: 'normal',
timeline: 'none',
timingFunction: 'ease',
},
}} />
)
}When both the animation property and animation-* properties are declared simultaneously, animation-* takes
precedence, as is the case with standard CSS. Since the animation object supports everything that animation-* can
set, it is recommended to use animation.
function Component () {
return (
<div style={{
animation: {
duration: '1s',
keyframes: {
from: {
opacity: 0,
},
to: {
opacity: 1,
},
}
},
animationDuration: '2s', // Not recommended, overrides the duration set in the animation object
}} />
)
}Unsupported things
Other features like :root and @font-face, it is recommended to keep these in separate CSS files that are imported
globally. There is no reason to handle them as inline styles.
