Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Aesthetic is a powerful React library for styling components through the use of adapters.
Aesthetic is a powerful React library for styling components, whether it be CSS-in-JS using objects, importing stylesheets, or simply referencing external class names. Simply put, Aesthetic is an abstraction layer that utilizes higher-order-components for the compilation of styles via third-party libraries, all the while providing customizability, theming, and a unified syntax.
import React from 'react';
import PropTypes from 'prop-types';
import { classes, ClassNamesPropType } from 'aesthetic';
import style from '../path/to/styler';
class Carousel extends React.Component {
static propTypes = {
children: PropTypes.node,
classNames: ClassNamesPropType,
};
// ...
render() {
const { children, classNames } = this.props;
const { animating } = this.state;
return (
<div
role="tablist"
className={classes({
classNames.carousel,
animating && classNames.carousel__animating,
})}
>
<ul className={classNames.list}>
{children}
</ul>
<button
type="button"
onClick={this.handlePrev}
className={classes(classNames.button, classNames.prev)}
>
←
</button>
<button
type="button"
onClick={this.handleNext}
className={classes(classNames.button, classNames.next)}
>
→
</button>
</div>
);
}
}
export default style({
carousel: {
position: 'relative',
maxWidth: '100%',
// ...
},
carousel__animating: { ... },
list: { ... },
button: { ... },
prev: { ... },
next: { ... },
})(Carousel);
Aesthetic was built for the sole purpose of solving the following scenarios, most of which competing styling libraries fail to solve.
Multiple styling patterns
Want to use external CSS or Sass files? Or maybe CSS modules? Or perhaps CSS-in-JS? What about JSS instead of Aphrodite? All of these patterns and choices are supported through the use of adapters. However, inline styles are not supported as we prefer the more performant option of compiling styles and attaching them to the DOM.
Styling third-party libraries
Using a third-party provided UI component library has the unintended side-effect of hard-coded and non-customizable styles. Aesthetic solves this by allowing consumers to extend and inherit styles from the provided base component.
Aesthetic requires React as a peer dependency.
npm install aesthetic react --save
// Or
yarn add aesthetic react
Aesthetic makes heavy use of process.env.NODE_ENV
for logging errors in development.
These errors will be entirely removed in production if the following build steps are configured.
DefinePlugin plugin is required when using Webpack.
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'),
}),
Envify transformer is required when using Browserify.
envify({
NODE_ENV: process.env.NODE_ENV || 'production',
});
An adapter in the context of Aesthetic is a third-party library that supports CSS in JavaScript, whether it be injecting CSS styles based off JavaScript objects, importing CSS during a build process, or simply referencing CSS class names.
The following libraries and their features are officially supported by Aesthetic.
Adapter | Unified Syntax | Pseudos | Fallbacks | Fonts | Animations | Media Queries | React Native |
---|---|---|---|---|---|---|---|
CSS class names | ✓ | ✓ | ✓ | ✓ | ✓ | ||
CSS modules | ✓ | ✓ | ✓ | ✓ | ✓ | ||
Aphrodite | ✓ | ✓ | ✓ | ✓ | ✓ | ||
Fela | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
Glamor | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
JSS | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
React Native | ✓ | ✓ | ✓ |
The following libraries are currently not supported.
To start using Aesthetic, a styler function must be created. This styler function acts as a factory for the creation of higher-order-components (HOC). These HOC's are used in transforming styles via adapters and passing down CSS class names to the original wrapped component.
To begin, we must create an instance of Aesthetic
with an adapter,
pass it to createStyler
, and export the new function. I suggest doing this an a file
that can be imported for reusability.
import Aesthetic, { createStyler } from 'aesthetic';
import JSSAdapter from 'aesthetic-adapter-jss'; // Or your chosen adapter
export default createStyler(new Aesthetic(new JSSAdapter()));
Once we have a styler function, we can import it and wrap our React components. The styler function accepts a style declaration as its first argument, and an object of configurable options as the second. The following options are supported.
styleName
(string) - The unique style name of the component. This name is primarily
used in logging and caching. Defaults to the component or function name.extendable
(boolean) - Allows the component and its styles to be extended,
creating a new component in the process. Defaults to false
.stylesPropName
(string) - Name of the prop in which the compiled class names or styles
object is passed to. Defaults to classNames
.themePropName
(string) - Name of the prop in which the theme name is passed to.
Defaults to theme
.pure
(boolean) - When true, the higher-order-component will extend React.PureComponent
instead of React.Component
. Only use this for static/dumb components.export default style({
button: { ... },
}, {
styleName: 'CustomButton',
extendable: true,
pure: true,
stylesPropName: 'classes',
themePropName: 'appTheme',
})(Button);
If you get tired of passing stylesPropName
, themePropName
, pure
, and extendable
to every component, you can pass these as default options to the Aesthetic
instance.
new Aesthetic(adapter, {
extendable: true,
pure: true,
stylesPropName: 'classes',
themePropName: 'appTheme',
})
Now that we have a styler function, we can start styling our components by wrapping
the component declaration with the styler function and passing an object of styles.
When this component is rendered, the style object is transformed into an object of class names,
and passed to the classNames
prop.
import React, { PropTypes } from 'react';
import { ClassNamesPropType } from 'aesthetic';
import style from '../path/to/styler';
function Button({ children, classNames, icon }) {
return (
<button type="button" className={classNames.button}>
{icon && (
<span className={classNames.icon}>{icon}</span>
)}
{children}
</button>
);
}
Button.propTypes = {
children: PropTypes.node,
classNames: ClassNamesPropType,
icon: PropTypes.node,
};
export default style({
button: { ... },
icon: { ... }
})(Button);
Since styles are isolated and colocated within a component, they can be impossible to
customize, especially if the component comes from a third-party library. If a component
styled by Aesthetic is marked as extendable
, styles can be customized by calling
the static extendStyles
method on the wrapped component instance.
Extending styles will return the original component wrapped with new styles, instead of wrapping the styled component and stacking on an unnecessary layer.
import BaseButton from '../path/to/styled/Button';
export const Button = BaseButton.extendStyles({
button: {
background: 'white',
// ...
},
});
export const PrimaryButton = BaseButton.extendStyles({
button: {
background: 'blue',
// ...
},
});
Parent styles (the component that was extended) are available when using a style function, allowing multiple layers of styles to be inherited.
When multiple class names need to be applied to a single element, the classes
function provided by Aesthetic can be used. This function accepts an arbitrary
number of arguments, all of which can be strings, arrays, or objects that evaluate to true.
import { classes } from 'aesthetic';
classes(
'foo',
expression && 'bar',
{
baz: false,
qux: true,
},
); // foo qux
Using our Button
examples above, let's add an active state and combine classes
like so. Specificity is important, so define styles from top to bottom!
function Button({ children, classNames, icon, active = false }) {
return (
<button
type="button"
className={classes(
classNames.button,
active && classNames.button__active,
)}
>
{icon && (
<span className={classNames.icon}>{icon}</span>
)}
{children}
</button>
);
}
As mentioned previously, to style a component, an object or function must be passed as the first argument to the styler function. This object represents a mapping of elements (and modifiers) to declarations. For example:
style({
button: { ... },
button__active: { ... },
icon: { ... },
})(Button)
The following types of declarations are permitted.
External CSS class names can be referenced by passing a string of the class name.
style({
button: 'button',
button__active: 'button--active',
icon: 'button__icon',
})(Button)
To make use of class names, the provided ClassNameAdapter
must be used.
import Aesthetic, { createStyler, ClassNameAdapter } from 'aesthetic';
export default createStyler(new Aesthetic(new ClassNameAdapter()));
CSS styles can be defined using an object of properties to values. These objects are transformed using adapters and optionally support the unified syntax defined by Aesthetic.
style({
button: {
background: '#eee',
// ...
},
button__active: {
background: '#fff',
// ...
},
icon: {
display: 'inline-block',
verticalAlign: 'middle',
// ...
},
})(Button)
Style functions are simply functions that return a style object. The benefits of using a function is that it provides the current theme as the first argument, and the previous styles as the second argument.
style((theme, prevStyles) => {
// ...
})(Button)
Themes are great in that they enable components to be styled in different ways based on pre-defined style guide parameters, like font size, color hex codes, and more.
To make use of a theme, register it through the Aesthetic
instance using registerTheme
.
This method accepts a name, an object of parameters, and an optional
style object used for globals (like font faces and animation keyframes).
aesthetic.registerTheme('dark', {
unit: 'em',
unitSize: 8,
spacing: 5,
font: 'Roboto',
bgColor: 'darkgray',
}, {
'@font-face': {
roboto: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'normal',
src: "url('roboto.woff2') format('roboto')",
},
},
});
Global styles are immediately compiled and attached the DOM. Be wary of conflicts.
If you'd like to extend a base theme to create a new theme, use extendTheme
. This
method accepts the theme name to inherit from as the first argument, with the remaining
arguments matching registerTheme
.
aesthetic.extendTheme('dark', 'darker', {
bgColor: 'black',
});
Extending themes will deep merge the two parameter objects.
Once a theme has been registered, we can access the style parameters by using a style function. The parameters object is passed as the first argument to the function.
style(theme => ({
button: {
fontSize: `${theme.unitSize}${theme.unit}`,
fontFamily: theme.font,
padding: theme.spacing,
},
}))(Component);
To activate and inform components to use a specific theme, we must use the ThemeProvider
,
which accepts a name
of the theme.
import { ThemeProvider } from 'aesthetic';
<ThemeProvider name="default">
// All components within here will use the "default" theme
<ThemeProvider name="dark">
// And all components here will use the "dark" theme
</ThemeProvider>
</ThemeProvider>
Or by passing a theme
prop to an individual component.
<Button theme="dark">Save</Button>
Or by setting the default theme on the Aesthetic
instance.
new Aesthetic(adapter, { defaultTheme: 'default' });
Aesthetic provides an optional unified CSS-in-JS syntax. This unified syntax permits easy drop-in replacements between adapters that utilize CSS-in-JS objects, as well as a standard across libraries.
Pros
Cons
Why a new syntax?
While implementing adapters and writing tests for all their syntax and use cases, I noticed that all adapters shared about 90-95% of the same syntax. That remaining percentage could easily be abstracted away by a library, and hence, this unified syntax was created.
Furthermore, a unified syntax allows providers of third-party components to define their styles in a standard way with consumers having the choice of their preferred adapter.
Why a different at-rule structure?
The major difference between the unified syntax and native adapters syntax, is that at-rules
in the unified syntax are now multi-dimensional objects indexed by the name of the at-rule
(@media
), while at-rules in the native syntax are single objects indexed by the at-rule
declaration (@media (min-width: 100px)
).
Supporting the native syntax incurred an linear (O(n)
) lookup, as we would have to loop
through each object recursively to find all at-rules, while the unified syntax is a simple
constant (O(1)
) lookup as we know the names ahead of time. This constant time lookup is
what enables a fast conversion process between the unified and native syntaxes.
How do I enable the unified syntax?
Please refer to the readme of your chosen adapter.
Standard structure for defining properties.
button: {
margin: 0,
padding: 5,
display: 'inline-block',
lineHeight: 'normal',
textAlign: 'center',
cursor: 'pointer',
backgroundColor: '#ccc',
color: '#000',
},
buttonGroup: {
// ...
},
JSS requires the
jss-default-unit
,jss-camel-case
, andjss-global
plugins.
Fela requires the
fela-plugin-unit
plugin.
Pseudo elements and classes are defined inside an element as nested objects.
button: {
// ...
':hover': {
backgroundColor: '#eee',
},
'::before': {
content: '"★"',
display: 'inline-block',
marginRight: 5,
},
},
JSS requires the
jss-nested
plugin.
Property fallbacks for old browsers are defined under the @fallbacks
object.
Each property accepts a single value or an array of values.
wrapper: {
// ...
background: 'linear-gradient(...)',
display: 'flex',
'@fallbacks': {
background: 'red',
display: ['box', 'flex-box'],
},
},
Aphrodite does not support fallback styles.
Fela requires the
fela-plugin-fallback-value
plugin.
Media queries are defined inside an element using a @media
object.
tooltip: {
// ...
maxWidth: 300,
'@media': {
'(min-width: 400px)': {
maxWidth: 'auto',
},
},
},
Font faces are defined outside the element using a @font-face
object
and are referenced by font family name.
'@font-face': {
roboto: {
fontFamily: 'Roboto',
fontStyle: 'normal',
fontWeight: 'normal',
src: "url('roboto.woff2') format('roboto')",
},
},
button: {
// ...
fontFamily: 'Roboto',
},
tooltip: {
// ...
fontFamily: 'Roboto, sans-serif',
},
Animation keyframes are defined outside the element using a @keyframes
object
and are referenced by animation name (the object key).
'@keyframes': {
fade: {
from: { opacity: 0 },
to: { opacity: 1 },
},
},
button: {
// ...
animationName: 'fade',
animationDuration: '3s',
},
Parent, child, and sibling selectors are purposefully not supported. Use unique and isolated element names and style declarations instead.
A brief comparison of Aesthetic to competing React style abstraction libraries.
aesthetic | react-with-styles | styled-components | radium | |
---|---|---|---|---|
Abstraction | HOC | HOC | Template Literals | HOC |
Type | Classes | Classes, Inline styles | Classes | Inline styles |
Unified Syntax | ✓ | |||
Caching | ✓ | ✓ | N/A | |
Themes | ✓ | ✓ | ✓ | |
Style Extending | ✓ | ✓ |
aesthetic | react-with-styles | styled-components | radium | |
---|---|---|---|---|
CSS class names | ✓ | |||
CSS Modules | ✓ | |||
Aphrodite | ✓ | ✓ | ||
Fela | ✓ | |||
Glamor | ✓ | ✓ | ||
JSS | ✓ | ✓ | ||
React Native | ✓ | ✓ |
Please refer to the aesthetic-native package for more information on how to integrate React Native with Aesthetic.
FAQs
Aesthetic is a powerful type-safe, framework agnostic, CSS-in-JS library for styling components through the use of adapters.
The npm package aesthetic receives a total of 90 weekly downloads. As such, aesthetic popularity was classified as not popular.
We found that aesthetic demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.