This is part of the Ext JS to React blog series. You can review the code from this article on the Ext JS to React Git repo.
Styling a React application will be familiar to Ext JS developers. Ext JS shipped with a number of predefined themes with variables enabling customization of each theme. If your React application uses views from a library such as Kendo UI or Semantic, you’ll likely use their tools and processes to refine any theme shipped with their views. However, when it comes to styling custom view components, you’ll own the styling of the components.
Having a theme provided to style all views within an application is certainly a convenience. However, most employers/clients will want a little (or a lot) of customization when it comes to look and feel. That could include colors used, fonts and font sizes, borders and shadows, and more that may depart from a shipped theme. So, whether the task is customizing a shipped theme or styling custom components from the ground up, you’ll likely need a strategy for managing view styles across your application.
In this blog article, we’ll discuss a few common strategies used by those in the React community such as CSS modules, inline styles, and styled-components.
Note: While not a requirement for React, this article’s code examples assume you’re starting with a starter app generated by create-react-app.
Simple Styling
The starter app uses an import statement to connect the stylesheet associated with the App component:
import './App.css';
Webpack reads these imports in during development or production builds. This pairing of CSS styling to each corresponding component improves file / component management versus using a single stylesheet to style an entire application and all of its components. If an application is small and doesn’t include components and their accompanying styles shared across multiple projects, this method of importing stylesheets may suffice. But, if the application is sufficiently large or employs shared views, further encapsulating the style rules associated with each view may be crucial. And for that, we’ll look at CSS Modules.
CSS Modules overview
CSS Modules loads CSS stylesheets in addition to pre-processed languages like Sass, Less, and PostCSS. This is similar to how JavaScript modules are loaded. A primary benefit of CSS Modules is that each CSS class is uniquely named within the build process, thus scoping the stylesheet to its paired React component. This unique naming mechanism completely insulates a component’ styles from all other styles in an application. CSS name collisions are effectively eliminated making atomically designed components easier to safely drop in to various applications or modules.
Up to this point we’ve been building our starter app using create-react-app and all of the configuration magic has been handled behind the scenes. But (at the time of this writing) there is no official way to enable CSS Modules without performing an eject on our starter app which takes create-react-app out of the config file driver’s seat and opens up the config options for us to tinker with. Ejecting is a one-time thing per application and there’s no going back to the days of create-react-app managing everything for us. After ejecting, the application is no longer eligible for automatic updates via create-react-app. We are now able to configure webpack per the specifics of our app.
In addition to encapsulating our components we’re going to look at how an application can be configured to work with a CSS preprocessor (in our case Sass, since that’s what we used when theming with Ext JS). As you build custom views and their associated styling you’ll have to decide how many dependencies you want to introduce, such as relying on application-wide variables and links between stylesheets. For the sake of covering this topic we’ll assume that we’re ok with some dependencies within a collection of views all meant to operate with the same theme constants.
CSS Modules setup
In this section we’re going to set up our application for CSS Modules. Follow along with a newly created app as we demonstrate how to configure an application for CSS Modules and Sass. Within the terminal from your application folder run:
npm run eject npm i sass-loader node-sass-chokidar --save
At this stage, we’ve ejected from the initial starter app and installed two modules used to work with Sass files. sass-loader is an addon to webpack that adds the ability to preprocess Sass files before feeding them to the CSS loader. node-sass-chokidar is an npm module that bundles node-sass and does all the hard work of preprocessing. Chokidar is a highly performant watcher that will ensure low overhead on a development machine.
The eject action exposes the Webpack config files we’ll need to modify in order to set up CSS Modules (Sass Modules). The config files we’ll need to edit are too large to reproduce here in their entirety so we’ll cover updating just the necessary bits. Start by opening {appRoot}/config/webpack.config.dev.js
and modifying it to look like:
{ test: /\.(scss|css)$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 2, modules: true, localIdentName: "[name]__[local]___[hash:base64:5]" }, }, { loader: require.resolve('postcss-loader'), options: { // Necessary for external CSS imports to work // https://github.com/facebookincubator/create-react-app/issues/2677 ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // React doesn't support IE8 anyway ], flexbox: 'no-2009', }), ], }, }, require.resolve('sass-loader') ], },
Note: Be sure to add the same updates/additions to the production Webpack config file at {appRoot}/configs/webpack.config.prod.js
taking care to preserve other configuration options present in the production config file that differ from the dev config file.
Finally, run:
npm install npm start
Defining application styles
At this stage, we are able to begin adding application-wide styling that our individual views can use. Let’s start by adding an {appRoot}/src/styles
folder that will hold our global style definitions used by all elements within the application. Add the file {appRoot}/src/styles/styles.scss
and add a global base and accent color:
$base-color: #7986cb; $accent-color: #f48fb1;
Next, add {appRoot}/src/styles/button.scss
for our common button style and add the following to the button.scss
file:
@import './styles.scss'; .button { background-color: $base-color; line-height: 32px; }
Next, let’s add a theme folder with styles that build on any base styling we’ve defined. Start by adding the folder and sub-folder {appRoot}/src/styles/themes/material
. Then add a theme-level styles file {appRoot}/src/styles/themes/material/styles.scss
. For now, we’ll just import the global styles. In the future, global overrides for the theme can live in this file:
@import './styles.scss';
Now, let’s create a button stylesheet for our material theme. Add the file {appRoot}/src/styles/themes/material/button.scss
and update it with the following style rules:
@import '../../button.scss'; .button { @extend .button; border-width: 0; font-family: Roboto, sans-serif; text-transform: uppercase; font-weight: 500; font-size: 14px; transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms; border-radius: 2px; background-color: transparent; } .button:hover { background-color: rgba(210, 210, 210, 0.33); }
We extended the .button
rule from the imported button.scss
file overriding any base styling as needed for our material button.
Next, we’ll add a Button
class at {appRoot}/src/Button.js
that will use our custom button styling:
import React from 'react'; import btnStyles from 'button.scss'; export default ({ children, className }) => ( <button className={btnStyles[className || 'button']}>{children}</button> );
Finally, let’s modify {appRoot}/src/App.js
to show a Button
:
import React from 'react'; import Button from './Button'; import './App.css'; export default () => ( <div> <Button className="button">Button</Button> </div> );
After saving all updates, you’ll notice that we’re getting an error saying that the “button.scss” module can’t be found. At this point the only way to load a module would be to link to it explicitly with absolute or relative pathing, something we don’t have to do when importing JavaScript modules. Let’s fix it so that all .css
and .scss
imports are sourced from the material theme for this application. First, press “ctrl-c” to stop the server spun up by “npm start”. In {AppRoot}/configs/webpack.config.dev.js
add the following constant:
const stylePath = path.resolve(__dirname, '../src/styles/themes/material');
Change the resolve.modules entry from:
modules: ['node_modules', paths.appNodeModules].concat( // It is guaranteed to exist because we tweak it in `env.js` process.env.NODE_PATH.split(path.delimiter).filter(Boolean) ),
To
modules: ['node_modules', paths.appNodeModules].concat( // It is guaranteed to exist because we tweak it in `env.js` process.env.NODE_PATH.split(path.delimiter).filter(Boolean), stylePath ),
Now, all of our CSS / Sass modules will source from the material theme folder. While we’re at it, let’s modify the webpack config file so that our Sass “@import” statements will not be required to use explicit pathing as well. Modify the “sass-loader” entry changing it from:
require.resolve('sass-loader')
To
{ loader: require.resolve('sass-loader'), options: { includePaths: [stylePath] } }
Note: Be sure to modify {appRoot}/configs/webpack.config.prod.js
at this time as well.
Now re-run npm run start
to see our changes take shape.
You can review the sample code on the Git repo.
Theming React Applications with CSS Modules
The theme styling can now easily be used to style our views. But what if we wanted an arbitrary element in a component to be able to take advantage of a theme’s global variables? Let’s demonstrate this by renaming the {appRoot}/src/App.css
file to {appRoot}/src/App.scss
and by changing the import statement in {appRoot}/src/App.js
to source from App.scss
. We’ll replace the contents of {appRoot}/src/App.scss
with:
@import 'styles.scss'; .myContainer { padding: 8px; background-color: $base-color; }
Here we import the global style rules for the theme including any global variables. We’ll use the $base-color
variable in a style rule of .myContainer
. Now, let’s update {appRoot}/src/App.js
to use the new style rule:
import React from 'react'; import Button from './Button'; import styles from './App.scss'; export default () => ( <div className={styles.myContainer}> <Button className="button">Button</Button> </div> );
Our Button
from before is now wrapped in a containing div
with a background color dictated by our theme. Additionally, we can create one-off Button
ui by adding a rule in our src/App.scss
file that composes from our theme’s .button
style and overrides select styles. First, let’s add another button to our App
component:
import React from 'react'; import Button from './Button'; import styles from './App.scss'; export default () => ( <div className={styles.myContainer}> <Button className="button">Button</Button> <Button ui={styles.myButton}>Button</Button> </div> );
Next, we’ll add a .myButton
style rule that uses the global $accent-color
plus the theme’s button styling. Add the following to the end of src/App.scss
:
.myButton { composes: button from 'button.scss'; background-color: $accent-color; } .myButton:hover { background-color: darken($accent-color, 10%); }
We now have a standard themed button followed by one with a pink background color. If this were a commonly needed UI we could add the style rule to the theme for shared use throughout the application.
Now that we’ve created a structure we can use for components going forward we could add another theme folder and create the needed component stylesheets there with whatever nuanced color, layout, or typography changes we want for the new theme. And since our component files and their accompanying stylesheets point to the module names we can change the theme folder we want to use by updating the stylePath
constant in the webpack config files. For example, we could create a “fabric” theme by adding an {appRoot}/src/styles/themes/fabric
folder and its component stylesheets and updating the webpack configs by changing from:
const stylePath = path.resolve(__dirname, '../src/styles/themes/material');
to
const stylePath = path.resolve(__dirname, '../src/styles/themes/fabric');
You can review the sample code on the Git repo.
Conclusion
Enabling the module-loading behavior with our CSS/Sass files required a little bit of customization to the configuration used by our starter app. The upside, though, is that it’s a small set of changes and only needs to be done the one time. With an application now set up to work with Sass module files you can create global styling for the application with view-specific style rules all with the confidence that all styling is completely encapsulated within each view. When creating a library of views with coordinating style modules to be shared across multiple projects, only the views and their styles imported by a given project will be output in the build process. Your build files will now be as lean as possible with a tidy, maintainable code base.
Mitchell Simoens
Related Posts
-
Ext JS to React: FAQ
This is part of the Ext JS to React blog series. React is Facebook's breakout…
-
Ext JS to React: Migration to Open Source
Worried about Migrating from Ext JS? Modus has the Answers Idera’s acquisition of Sencha has…