Tag: Multiple

How to Make a Component That Supports Multiple Frameworks in a Monorepo

Your mission — should you decide to accept it — is to build a Button component in four frameworks, but, only use one button.css file!

This idea is very important to me. I’ve been working on a component library called AgnosticUI where the purpose is building UI components that aren’t tied to any one particular JavaScript framework. AgnosticUI works in React, Vue 3, Angular, and Svelte. So that’s exactly what we’ll do today in this article: build a button component that works across all these frameworks.

The source code for this article is available on GitHub on the the-little-button-that-could-series branch.

Table of contents

Why a monorepo?

We’re going to set up a tiny Yarn workspaces-based monorepo. Why? Chris actually has a nice outline of the benefits in another post. But here’s my own biased list of benefits that I feel are relevant for our little buttons endeavor:

Coupling

We’re trying to build a single button component that uses just one button.css file across multiple frameworks. So, by nature, there’s some purposeful coupling going on between the various framework implementations and the single-source-of-truth CSS file. A monorepo setup provides a convenient structure that facilitates copying our single button.css component into various framework-based projects.

Workflow

Let’s say the button needs a tweak — like the “focus-ring” implementation, or we screwed up the use of aria in the component templates. Ideally, we’d like to correct things in one place rather than making individual fixes in separate repositories.

Testing

We want the convenience of firing up all four button implementations at the same time for testing. As this sort of project grows, it’s safe to assume there will be more proper testing. In AgnosticUI, for example, I’m currently using Storybook and often kick off all the framework Storybooks, or run snapshot testing across the entire monorepo.

I like what Leonardo Losoviz has to say about the monorepo approach. (And it just so happens to align with with everything we’ve talked about so far.)

I believe the monorepo is particularly useful when all packages are coded in the same programming language, tightly coupled, and relying on the same tooling.

Setting up

Time to dive into code — start by creating a top-level directory on the command-line to house the project and then cd into it. (Can’t think of a name? mkdir buttons && cd buttons will work fine.)

First off, let’s initialize the project:

$  yarn init yarn init v1.22.15 question name (articles): littlebutton question version (1.0.0):  question description: my little button project question entry point (index.js):  question repository url:  question author (Rob Levin):  question license (MIT):  question private:  success Saved package.json

That gives us a package.json file with something like this:

{   "name": "littlebutton",   "version": "1.0.0",   "description": "my little button project",   "main": "index.js",   "author": "Rob Levin",   "license": "MIT" }

Creating the baseline workspace

We can set the first one up with this command:

mkdir -p ./littlebutton-css

Next, we need to add the two following lines to the monorepo’s top-level package.json file so that we keep the monorepo itself private. It also declares our workspaces:

// ... "private": true, "workspaces": ["littlebutton-react", "littlebutton-vue", "littlebutton-svelte", "littlebutton-angular", "littlebutton-css"]

Now descend into the littlebutton-css directory. We’ll again want to generate a package.json with yarn init. Since we’ve named our directory littlebutton-css (the same as how we specified it in our workspaces in package.json) we can simply hit the Return key and accept all the prompts:

$  cd ./littlebutton-css && yarn init yarn init v1.22.15 question name (littlebutton-css):  question version (1.0.0):  question description:  question entry point (index.js):  question repository url:  question author (Rob Levin):  question license (MIT):  question private:  success Saved package.json

At this point, the directory structure should look like this:

├── littlebutton-css │   └── package.json └── package.json

We’ve only created the CSS package workspace at this point as we’ll be generating our framework implementations with tools like vite which, in turn, generate a package.json and project directory for you. We will have to remember that the name we choose for these generated projects must match the name we’ve specified in the package.json for our earlier workspaces to work.

Baseline HTML & CSS

Let’s stay in the ./littlebutton-css workspace and create our simple button component using vanilla HTML and CSS files.

touch index.html ./css/button.css

Now our project directory should look like this:

littlebutton-css ├── css │   └── button.css ├── index.html └── package.json

Let’s go ahead and connect some dots with some boilerplate HTML in ./index.html:

<!doctype html> <html lang="en"> <head>   <meta charset="utf-8">   <title>The Little Button That Could</title>   <meta name="description" content="">   <meta name="viewport" content="width=device-width, initial-scale=1">   <link rel="stylesheet" href="css/button.css"> </head> <body>   <main>     <button class="btn">Go</button>   </main> </body> </html>

And, just so we have something visual to test, we can add a little color in ./css/button.css:

.btn {   color: hotpink; }
A mostly unstyled button with hot-pink text from the monorepo framework.

Now open up that index.html page in the browser. If you see an ugly generic button with hotpink text… success!

Framework-specific workspaces

So what we just accomplished is the baseline for our button component. What we want to do now is abstract it a bit so it’s extensible for other frameworks and such. For example, what if we want to use the button in a React project? We’re going to need workspaces in our monorepo for each one. We’ll start with React, then follow suit for Vue 3, Angular, and Svelte.

React

We’re going to generate our React project using vite, a very lightweight and blazingly fast builder. Be forewarned that if you attempt to do this with create-react-app, there’s a very good chance you will run into conflicts later with react-scripts and conflicting webpack or Babel configurations from other frameworks, like Angular.

To get our React workspace going, let’s go back into the terminal and cd back up to the top-level directory. From there, we’ll use vite to initialize a new project — let’s call it littlebutton-react — and, of course, we’ll select react as the framework and variant at the prompts:

$  yarn create vite yarn create v1.22.15 [1/4] 🔍  Resolving packages... [2/4] 🚚  Fetching packages... [3/4] 🔗  Linking dependencies... [4/4] 🔨  Building fresh packages...  success Installed "create-vite@2.6.6" with binaries:       - create-vite       - cva ✔ Project name: … littlebutton-react ✔ Select a framework: › react ✔ Select a variant: › react  Scaffolding project in /Users/roblevin/workspace/opensource/guest-posts/articles/littlebutton-react...  Done. Now run:    cd littlebutton-react   yarn   yarn dev  ✨  Done in 17.90s.

We initialize the React app with these commands next:

cd littlebutton-react yarn yarn dev

With React installed and verified, let’s replace the contents of src/App.jsx to house our button with the following code:

import "./App.css";  const Button = () => {   return <button>Go</button>; };  function App() {   return (     <div className="App">       <Button />     </div>   ); }  export default App;

Now we’re going to write a small Node script that copies our littlebutton-css/css/button.css right into our React application for us. This step is probably the most interesting one to me because it’s both magical and ugly at the same time. It’s magical because it means our React button component is truly deriving its styles from the same CSS written in the baseline project. It’s ugly because, well, we are reaching up out of one workspace and grabbing a file from another. ¯_(ツ)_/¯

Add the following little Node script to littlebutton-react/copystyles.js:

const fs = require("fs"); let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8"); fs.writeFileSync("./src/button.css", css, "utf8");

Let’s place a node command to run that in a package.json script that happens before the dev script in littlebutton-react/package.json. We’ll add a syncStyles and update the dev to call syncStyles before vite:

"syncStyles": "node copystyles.js", "dev": "yarn syncStyles && vite",

Now, anytime we fire up our React application with yarn dev, we’ll first be copying the CSS file over. In essence, we’re “forcing” ourselves to not diverge from the CSS package’s button.css in our React button.

But we want to also leverage CSS Modules to prevent name collisions and global CSS leakage, so we have one more step to do to get that wired up (from the same littlebutton-react directory):

touch src/button.module.css

Next, add the following to the new src/button.module.css file:

.btn {   composes: btn from './button.css'; }

I find composes (also known as composition) to be one of the coolest features of CSS Modules. In a nutshell, we’re copying our HTML/CSS version of button.css over wholesale then composing from our one .btn style rule.

With that, we can go back to our src/App.jsx and import the CSS Modules styles into our React component with this:

import "./App.css"; import styles from "./button.module.css";  const Button = () => {   return <button className={styles.btn}>Go</button>; };  function App() {   return (     <div className="App">       <Button />     </div>   ); }  export default App;

Whew! Let’s pause and try to run our React app again:

yarn dev

If all went well, you should see that same generic button, but with hotpink text. Before we move on to the next framework, let’s move back up to our top-level monorepo directory and update its package.json:

{   "name": "littlebutton",   "version": "1.0.0",   "description": "toy project",   "main": "index.js",   "author": "Rob Levin",   "license": "MIT",   "private": true,   "workspaces": ["littlebutton-react", "littlebutton-vue", "littlebutton-svelte", "littlebutton-angular"],   "scripts": {     "start:react": "yarn workspace littlebutton-react dev"   } }

Run the yarn command from the top-level directory to get the monorepo-hoisted dependencies installed.

The only change we’ve made to this package.json is a new scripts section with a single script to start the React app. By adding start:react we can now run yarn start:react from our top-level directory and it will fire up the project we just built in ./littlebutton-react without the need for cd‘ing — super convenient!

We’ll tackle Vue and Svelte next. It turns out that we can take a pretty similar approach for these as they both use single file components (SFC). Basically, we get to mix HTML, CSS, and JavaScript all into one single file. Whether you like the SFC approach or not, it’s certainly adequate enough for building out presentational or primitive UI components.

Vue

Following the steps from vite’s scaffolding docs we’ll run the following command from the monorepo’s top-level directory to initialize a Vue app:

yarn create vite littlebutton-vue --template vue

This generates scaffolding with some provided instructions to run the starter Vue app:

cd littlebutton-vue yarn yarn dev

This should fire up a starter page in the browser with some heading like “Hello Vue 3 + Vite.” From here, we can update src/App.vue to:

<template>   <div id="app">     <Button class="btn">Go</Button>   </div> </template>  <script> import Button from './components/Button.vue'  export default {   name: 'App',   components: {     Button   } } </script>

And we’ll replace any src/components/* with src/components/Button.vue:

<template>   <button :class="classes"><slot /></button> </template>  <script> export default {   name: 'Button',   computed: {     classes() {       return {         [this.$ style.btn]: true,       }     }   } } </script>  <style module> .btn {   color: slateblue; } </style>

Let’s break this down a bit:

  • :class="classes" is using Vue’s binding to call the computed classes method.
  • The classes method, in turn, is utilizing CSS Modules in Vue with the this.$ style.btn syntax which will use styles contained in a <style module> tag.

For now, we’re hardcoding color: slateblue simply to test that things are working properly within the component. Try firing up the app again with yarn dev. If you see the button with our declared test color, then it’s working!

Now we’re going to write a Node script that copies our littlebutton-css/css/button.css into our Button.vue file similar to the one we did for the React implementation. As mentioned, this component is a SFC so we’re going to have to do this a little differently using a simple regular expression.

Add the following little Node.js script to littlebutton-vue/copystyles.js:

const fs = require("fs"); let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8"); const vue = fs.readFileSync("./src/components/Button.vue", "utf8"); // Take everything between the starting and closing style tag and replace const styleRegex = /<style module>([sS]*?)</style>/; let withSynchronizedStyles = vue.replace(styleRegex, `<style module>n$ {css}n</style>`); fs.writeFileSync("./src/components/Button.vue", withSynchronizedStyles, "utf8");

There’s a bit more complexity in this script, but using replace to copy text between opening and closing style tags via regex isn’t too bad.

Now let’s add the following two scripts to the scripts clause in the littlebutton-vue/package.json file:

"syncStyles": "node copystyles.js", "dev": "yarn syncStyles && vite",

Now run yarn syncStyles and look at ./src/components/Button.vue again. You should see that our style module gets replaced with this:

<style module> .btn {   color: hotpink; } </style>

Run the Vue app again with yarn dev and verify you get the expected results — yes, a button with hotpink text. If so, we’re good to move on to the next framework workspace!

Svelte

Per the Svelte docs, we should kick off our littlebutton-svelte workspace with the following, starting from the monorepo’s top-level directory:

npx degit sveltejs/template littlebutton-svelte cd littlebutton-svelte yarn && yarn dev

Confirm you can hit the “Hello World” start page at http://localhost:5000. Then, update littlebutton-svelte/src/App.svelte:

<script>   import Button from './Button.svelte'; </script> <main>   <Button>Go</Button> </main>

Also, in littlebutton-svelte/src/main.js, we want to remove the name prop so it looks like this:

import App from './App.svelte';  const app = new App({   target: document.body });  export default app;

And finally, add littlebutton-svelte/src/Button.svelte with the following:

<button class="btn">   <slot></slot> </button>  <script> </script>  <style>   .btn {     color: saddlebrown;   } </style>

One last thing: Svelte appears to name our app: "name": "svelte-app" in the package.json. Change that to "name": "littlebutton-svelte" so it’s consistent with the workspaces name in our top-level package.json file.

Once again, we can copy our baseline littlebutton-css/css/button.css into our Button.svelte. As mentioned, this component is a SFC, so we’re going to have to do this using a regular expression. Add the following Node script to littlebutton-svelte/copystyles.js:

const fs = require("fs"); let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8"); const svelte = fs.readFileSync("./src/Button.svelte", "utf8"); const styleRegex = /<style>([sS]*?)</style>/; let withSynchronizedStyles = svelte.replace(styleRegex, `<style>n$ {css}n</style>`); fs.writeFileSync("./src/Button.svelte", withSynchronizedStyles, "utf8");

This is super similar to the copy script we used with Vue, isn’t it? We’ll add similar scripts to our package.json script:

"dev": "yarn syncStyles && rollup -c -w", "syncStyles": "node copystyles.js",

Now run yarn syncStyles && yarn dev. If all is good, we once again should see a button with hotpink text.

If this is starting to feel repetitive, all I have to say is welcome to my world. What I’m showing you here is essentially the same process I’ve been using to build my AgnosticUI project!

Angular

You probably know the drill by now. From the monorepo’s top-level directory, install Angular and create an Angular app. If we were creating a full-blown UI library we’d likely use ng generate library or even nx. But to keep things as straightforward as possible we’ll set up a boilerplate Angular app as follows:

npm install -g @angular/cli ### unless you already have installed ng new littlebutton-angular ### choose no for routing and CSS ? Would you like to add Angular routing? (y/N) N ❯ CSS    SCSS   [ https://sass-lang.com/documentation/syntax#scss ]    Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]    Less   [ http://lesscss.org ]  cd littlebutton-angular && ng serve --open

With the Angular setup confirmed, let’s update some files. cd littlebutton-angular, delete the src/app/app.component.spec.ts file, and add a button component in src/components/button.component.ts, like this:

import { Component } from '@angular/core';  @Component({   selector: 'little-button',   templateUrl: './button.component.html',   styleUrls: ['./button.component.css'], }) export class ButtonComponent {}

Add the following to src/components/button.component.html:

<button class="btn">Go</button>

And put this in the src/components/button.component.css file for testing:

.btn {   color: fuchsia; }

In src/app/app.module.ts:

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser';  import { AppComponent } from './app.component'; import { ButtonComponent } from '../components/button.component';  @NgModule({   declarations: [AppComponent, ButtonComponent],   imports: [BrowserModule],   providers: [],   bootstrap: [AppComponent], }) export class AppModule {}

Next, replace src/app/app.component.ts with:

import { Component } from '@angular/core';  @Component({   selector: 'app-root',   templateUrl: './app.component.html',   styleUrls: ['./app.component.css'], }) export class AppComponent {}

Then, replace src/app/app.component.html with:

<main>   <little-button>Go</little-button> </main>

With that, let’s run yarn start and verify our button with fuchsia text renders as expected.

Again, we want to copy over the CSS from our baseline workspace. We can do that by adding this to littlebutton-angular/copystyles.js:

const fs = require("fs"); let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8"); fs.writeFileSync("./src/components/button.component.css", css, "utf8");

Angular is nice in that it uses ViewEncapsulation that defaults to to emulate which mimics, according to the docs,

[…] the behavior of shadow DOM by preprocessing (and renaming) the CSS code to effectively scope the CSS to the component’s view.

This basically means we can literally copy over button.css and use it as-is.

Finally, update the package.json file by adding these two lines in the scripts section:

"start": "yarn syncStyles && ng serve", "syncStyles": "node copystyles.js",

With that, we can now run yarn start once more and verify our button text color (which was fuchsia) is now hotpink.

What have we just done?

Let’s take a break from coding and think about the bigger picture and what we’ve just done. Basically, we’ve set up a system where any changes to our CSS package’s button.css will get copied over into all the framework implementations as a result of our copystyles.js Node scripts. Further, we’ve incorporated idiomatic conventions for each of the frameworks:

  • SFC for Vue and Svelte
  • CSS Modules for React (and Vue within the SFC <style module> setup)
  • ViewEncapsulation for Angular

Of course I state the obvious that these aren’t the only ways to do CSS in each of the above frameworks (e.g. CSS-in-JS is a popular choice), but they are certainly accepted practices and are working quite well for our greater goal — to have a single CSS source of truth to drive all framework implementations.

If, for example, our button was in use and our design team decided we wanted to change from 4px to 3px border-radius, we could update the one file, and any separate implementations would stay synced.

This is compelling if you have a polyglot team of developers that enjoy working in multiple frameworks, or, say an offshore team (that’s 3× productive in Angular) that’s being tasked to build a back-office application, but your flagship product is built in React. Or, you’re building an interim admin console and you’d love to experiment with using Vue or Svelte. You get the picture.

Finishing touches

OK, so we have the monorepo architecture in a really good spot. But there’s a few things we can do to make it even more useful as far as the developer experience goes.

Better start scripts

Let’s move back up to our top-level monorepo directory and update its package.json scripts section with the following so we can kick any framework implementation without cd‘ing:

// ... "scripts": {   "start:react": "yarn workspace littlebutton-react dev",   "start:vue": "yarn workspace littlebutton-vue dev ",   "start:svelte": "yarn workspace littlebutton-svelte dev",   "start:angular": "yarn workspace littlebutton-angular start" },

Better baseline styles

We can also provide a better set of baseline styles for the button so it starts from a nice, neutral place. Here’s what I did in the littlebutton-css/css/button.css file.

View Full Snippet
.btn {   --button-dark: #333;   --button-line-height: 1.25rem;   --button-font-size: 1rem;   --button-light: #e9e9e9;   --button-transition-duration: 200ms;   --button-font-stack:     system-ui,     -apple-system,     BlinkMacSystemFont,     "Segoe UI",     Roboto,     Ubuntu,     "Helvetica Neue",     sans-serif;    display: inline-flex;   align-items: center;   justify-content: center;   white-space: nowrap;   user-select: none;   appearance: none;   cursor: pointer;   box-sizing: border-box;   transition-property: all;   transition-duration: var(--button-transition-duration);   color: var(--button-dark);   background-color: var(--button-light);   border-color: var(--button-light);   border-style: solid;   border-width: 1px;   font-family: var(--button-font-stack);   font-weight: 400;   font-size: var(--button-font-size);   line-height: var(--button-line-height);   padding-block-start: 0.5rem;   padding-block-end: 0.5rem;   padding-inline-start: 0.75rem;   padding-inline-end: 0.75rem;   text-decoration: none;   text-align: center; }  /* Respect users reduced motion preferences */ @media (prefers-reduced-motion) {   .btn {     transition-duration: 0.001ms !important;   } }

Let’s test this out! Fire up each of the four framework implementations with the new and improved start scripts and confirm the styling changes are in effect.

Neutral (gray) styled button from the monorepo framework

One CSS file update proliferated to four frameworks — pretty cool, eh!?

Set a primary mode

We’re going to add a mode prop to each of our button’s and implement primary mode next. A primary button could be any color but we’ll go with a shade of green for the background and white text. Again, in the baseline stylesheet:

.btn {   --button-primary: #14775d;   --button-primary-color: #fff;   /* ... */ }

Then, just before the @media (prefers-reduced-motion) query, add the following btn-primary to the same baseline stylesheet:

.btn-primary {   background-color: var(--button-primary);   border-color: var(--button-primary);   color: var(--button-primary-color); }

There we go! Some developer conveniences and better baseline styles!

Updating each component to take a mode property

Now that we’ve added our new primary mode represented by the .btn-primary class, we want to sync the styles for all four framework implementations. So, let’s add some more package.json scripts to our top level scripts:

"sync:react": "yarn workspace littlebutton-react syncStyles", "sync:vue": "yarn workspace littlebutton-vue syncStyles", "sync:svelte": "yarn workspace littlebutton-svelte syncStyles", "sync:angular": "yarn workspace littlebutton-angular syncStyles"

Be sure to respect JSON’s comma rules! Depending on where you place these lines within your scripts: {...}, you’ll want to make sure there are no missing or trailing commas.

Go ahead and run the following to fully synchronize the styles:

yarn sync:angular && yarn sync:react && yarn sync:vue && yarn sync:svelte

Running this doesn’t change anything because we haven’t applied the primary class yet, but you should at least see the CSS has been copied over if you go look at the framework’s button component CSS.

React

If you haven’t already, double-check that the updated CSS got copied over into littlebutton-react/src/button.css. If not, you can run yarn syncStyles. Note that if you forget to run yarn syncStyles our dev script will do this for us when we next start the application anyway:

"dev": "yarn syncStyles && vite",

For our React implementation, we additionally need to add a composed CSS Modules class in littlebutton-react/src/button.module.css that is composed from the new .btn-primary:

.btnPrimary {   composes: btn-primary from './button.css'; }

We’ll also update littlebutton-react/src/App.jsx:

import "./App.css"; import styles from "./button.module.css";  const Button = ({ mode }) => {   const primaryClass = mode ? styles[`btn$ {mode.charAt(0).toUpperCase()}$ {mode.slice(1)}`] : '';   const classes = primaryClass ? `$ {styles.btn} $ {primaryClass}` : styles.btn;   return <button className={classes}>Go</button>; };  function App() {   return (     <div className="App">       <Button mode="primary" />     </div>   ); }  export default App;

Fire up the React app with yarn start:react from the top-level directory. If all goes well, you should now see your green primary button.

A dark green button with white text positioning in the center of the screen.

As a note, I’m keeping the Button component in App.jsx for brevity. Feel free to tease out the Button component into its own file if that bothers you.

Vue

Again, double-check that the button styles were copied over and, if not, run yarn syncStyles.

Next, make the following changes to the <script> section of littlebutton-vue/src/components/Button.vue:

<script> export default {   name: 'Button',   props: {     mode: {       type: String,       required: false,       default: '',       validator: (value) => {         const isValid = ['primary'].includes(value);         if (!isValid) {           console.warn(`Allowed types for Button are primary`);         }         return isValid;       },     }   },   computed: {     classes() {       return {         [this.$ style.btn]: true,         [this.$ style['btn-primary']]: this.mode === 'primary',       }     }   } } </script>

Now we can update the markup in littlebutton-vue/src/App.vue to use the new mode prop:

<Button mode="primary">Go</Button>

Now you can yarn start:vue from the top-level directory and check for the same green button.

Svelte

Let’s cd into littlebutton-svelte and verify that the styles in littlebutton-svelte/src/Button.svelte have the new .btn-primary class copied over, and yarn syncStyles if you need to. Again, the dev script will do that for us anyway on the next startup if you happen to forget.

Next, update the Svelte template to pass the mode of primary. In src/App.svelte:

<script>   import Button from './Button.svelte'; </script> <main>   <Button mode="primary">Go</Button> </main>

We also need to update the top of our src/Button.svelte component itself to accept the mode prop and apply the CSS Modules class:

<button class="{classes}">   <slot></slot> </button> <script>   export let mode = "";   const classes = [     "btn",     mode ? `btn-$ {mode}` : "",   ].filter(cls => cls.length).join(" "); </script>

Note that the <styles> section of our Svelte component shouldn’t be touched in this step.

And now, you can yarn dev from littlebutton-svelte (or yarn start:svelte from a higher directory) to confirm the green button made it!

Angular

Same thing, different framework: check that the styles are copied over and run yarn syncStyles if needed.

Let’s add the mode prop to the littlebutton-angular/src/app/app.component.html file:

<main>   <little-button mode="primary">Go</little-button> </main>

Now we need to set up a binding to a classes getter to compute the correct classes based on if the mode was passed in to the component or not. Add this to littlebutton-angular/src/components/button.component.html (and note the binding is happening with the square brackets):

<button [class]="classes">Go</button>

Next, we actually need to create the classes binding in our component at littlebutton-angular/src/components/button.component.ts:

import { Component, Input } from '@angular/core';  @Component({   selector: 'little-button',   templateUrl: './button.component.html',   styleUrls: ['./button.component.css'], }) export class ButtonComponent {   @Input() mode: 'primary' | undefined = undefined;    public get classes(): string {     const modeClass = this.mode ? `btn-$ {this.mode}` : '';     return [       'btn',       modeClass,     ].filter(cl => cl.length).join(' ');   } }

We use the Input directive to take in the mode prop, then we create a classes accessor which adds the mode class if it’s been passed in.

Fire it up and look for the green button!

Code complete

If you’ve made it this far, congratulations — you’ve reached code complete! If something went awry, I’d encourage you to cross-reference the source code over at GitHub on the the-little-button-that-could-series branch. As bundlers and packages have a tendency to change abruptly, you might want to pin your package versions to the ones in this branch if you happen to experience any dependency issues.

Take a moment to go back and compare the four framework-based button component implementations we just built. They’re still small enough to quickly notice some interesting differences in how props get passed in, how we bind to props, and how CSS name collisions are prevented among other subtle differences. As I continue to add components to AgnosticUI (which supports these exact same four frameworks), I’m continually pondering which offers the best developer experience. What do you think?

Homework

If you’re the type that likes to figure things out on your own or enjoys digging in deeper, here are ideas.

Button states

The current button styles do not account for various states, like :hover. I believe that’s a good first exercise.

/* You should really implement the following states    but I will leave it as an exercise for you to     decide how to and what values to use. */ .btn:focus {   /* If you elect to remove the outline, replace it      with another proper affordance and research how      to use transparent outlines to support windows      high contrast   */ } .btn:hover { } .btn:visited { } .btn:active { } .btn:disabled { }

Variants

Most button libraries support many button variations for things like sizes, shapes, and colors. Try creating more than the primary mode we already have. Maybe a secondary variation? A warning or success? Maybe filled and outline? Again, you can look at AgnosticUI’s buttons page for ideas.

CSS custom properties

If you haven’t started using CSS custom properties yet, I’d strongly recommend it. You can start by having a look at AgnosticUI’s common styles. I heavily lean on custom properties in there. Here are some great articles that cover what custom properties are and how you might leverage them:

Types

No… not typings, but the <button> element’s type attribute. We didn’t cover that in our component but there’s an opportunity to extend the component to other use cases with valid types, like button, submit, and reset. This is pretty easy to do and will greatly improve the button’s API.

More ideas

Gosh, you could do so much — add linting, convert it to Typescript, audit the accessibility, etc.

The current Svelte implementation is suffering from some pretty loose assumptions as we have no defense if the valid primary mode isn’t passed — that would produce a garbage CSS class:

mode ? `btn-$ {mode}` : "",

You could say, “Well, .btn-garbage as a class isn’t exactly harmful.” But it’s probably a good idea to style defensively when and where possible.

Potential pitfalls

There are some things you should be aware of before taking this approach further:

  • Positional CSS based on the structure of the markup will not work well for the CSS Modules based techniques used here.
  • Angular makes positional techniques even harder as it generates :host element representing each component view. This means you have these extra elements in between your template or markup structure. You’ll need to work around that.
  • Copying styles across workspace packages is a bit of an anti-pattern to some folks. I justify it because I believe the benefits outweigh the costs; also, when I think about how monorepos use symlinks and (not-so-failproof) hoisting, I don’t feel so bad about this approach.
  • You’ll have to subscribe to the decoupled techniques used here, so no CSS-in-JS.

I believe that all approaches to software development have their pros and cons and you ultimately have to decide if sharing a single CSS file across frameworks works for you or your specific project. There are certainly other ways you could do this (e.g. using littlebuttons-css as an npm package dependency) if needed.

Conclusion

Hopefully I’ve whet your appetite and you’re now really intrigued to create UI component libraries and/or design systems that are not tied to a particular framework. Maybe you have a better idea on how to achieve this — I’d love to hear your thoughts in the comments!

I’m sure you’ve seen the venerable TodoMVC project and how many framework implementations have been created for it. Similarly, wouldn’t it be nice to have a UI component library of primitives available for many frameworks? Open UI is making great strides to properly standardize native UI component defaults, but I believe we’ll always need to insert ourselves to some extent. Certainly, taking a good year to build a custom design system is quickly falling out of favor and companies are seriously questioning their ROI. Some sort of scaffolding is required to make the endeavor practical.

The vision of AgnosticUI is to have a relatively agnostic way to build design systems quickly that are not tied down to a particular frontend framework. If you’re compelled to get involved, the project is still very early and approachable and I’d love some help! Plus, you’re already pretty familiar with the how the project works now that you’ve gone through this tutorial!


How to Make a Component That Supports Multiple Frameworks in a Monorepo originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , ,

Easy Dark Mode (and Multiple Color Themes!) in React

I was working on a large React application for a startup, and aside from just wanting some good strategies to keep our styles organized, I wanted to give this whole “dark mode” thing a shot. With the huge ecosystem around React, you might think that there would be a go-to solution for style themes, but a little web searching shows that really isn’t the case.

There are plenty of different options out there, but many of them tie into very specific CSS strategies, like using CSS Modules, some form of CSS-in-JS, etc. I also found tools specific to certain frameworks, like Gatsby, but not a generic React project. What I was looking for was a basic system that’s easy to set up and work with without jumping through a ton of hoops; something fast, something easy to get a whole team of front-end and full-stack developers onboarded with quickly.

The existing solution I liked the best centered around using CSS variables and data attributes, found in this StackOverflow answer. But that also relied on some useRef stuff that felt hack-y. As they say in every infomercial ever, there’s got to be a better way!

Fortunately, there is. By combining that general CSS variable strategy with the beautiful useLocalStorage hook, we have a powerful, easy-to-use theming system. I’m going to walk through setting this thing up and running it, starting from a brand new React app. And if you stick around to the end, I also show you how to integrate it with react-scoped-css, which is what makes this my absolutely preferred way to work with CSS in React.

Project setup

Let’s pick this up at a very good place to start: the beginning.

This guide assumes a basic familiarity with CSS, JavaScript, and React.

First, make sure you have a recent version of Node and npm installed. Then navigate to whatever folder you want your project to live in, run git bash there (or your preferred command line tool), then run:

npx create-react-app easy-react-themes --template typescript

Swap out easy-react-themes with the name of your project, and feel free to leave off the --template typescript if you’d rather work in JavaScript. I happen to like TypeScript but it genuinely makes no difference for this guide, other than files ending in .ts/.tsx vs .js/.jsx.

Now we’ll open up our brand new project in a code editor. I’m using VS Code for this example, and if you are too, then you can run these commands:

cd easy-react-themes code .
Not much to look at yet, but we’ll change that!

Running npm start next starts your development server, and produces this in a new browser window:

And, finally, go ahead and install the use-local-storage package with:

npm i use-local-storage

And that’s it for the initial setup of the project!

Code setup

Open the App.tsx file and get rid of the stuff we don’t need.

We want to go from this…

…to this.

Delete the entire content in App.css:

Woot! Now let’s create our themes! Open up the index.css file and add this to it:

:root {   --background: white;   --text-primary: black;   --text-secondary: royalblue;   --accent: purple; } [data-theme='dark'] {   --background: black;   --text-primary: white;   --text-secondary: grey;   --accent: darkred; }

Here’s what we have so far:

See what we just did there? If you’re unfamiliar with CSS Custom Properties (as also known as CSS variables), they allow us to define a value to be used elsewhere in our stylesheets, with the pattern being --key: value. In this case, we’re only defining a few colors and applying them to the :root element so they can be used be used wherever else we need them across the whole React project.

The second part, starting with [data-theme='dark'], is where things get interesting. HTML (and JSX, which we’re using to create HTML in React) allows us to set completely arbitrary properties for our HTML elements with the data-* attribute. In this case, we are giving the outermost <div> element of our application a data-theme attribute and toggling its value between light and dark. When it’s dark, the CSS[data-theme='dark'] section overrides the variables we defined in the :root, so any styling which relies on those variables is toggled as well.

Let’s put that into practice. Back in App.tsx, let’s give React a way to track the theme state. We’d normally use something like useState for local state, or Redux for global state management, but we also want the user’s theme selection to stick around if they leave our app and come back later. While we could use Redux and redux-persist, that’s way overkill for our needs.

Instead, we’re using the useLocalStorage hook we installed earlier. It gives us a way to store things in local storage, as you might expect, but as a React hook, it maintains stateful knowledge of what it’s doing with localStorage, making our lives easy.

Some of you might be thinking “Oh no, what if the page renders before our JavaScript checks in with localStorage and we get the dreaded “flash of wrong theme?” But you don’t have to worry about that here since our React app is completely rendered client-side; the initial HTML file is basically a skeleton with a with a single <div> that React attaches the app to. All of the final HTML elements are generated by JavaScript after checking localStorage.

So, first, import the hook at the top of App.tsx with:

import useLocalStorage from 'use-local-storage'

Then, inside our App component, we use it with:

const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const [theme, setTheme] = useLocalStorage('theme', defaultDark ? 'dark' : 'light'); 

This does a few things for us. First, we’re checking if the user has set a theme preference in their browser settings. Then we’re creating a stateful theme variable that is tied to localStorage and the setTheme function to update theme. useLocalStorage adds a key:value pair to localStorage if it doesn’t already exist, which defaults to theme: "light", unless our matchMedia check comes back as true, in which case it’s theme: "dark". That way, we’re gracefully handling both possibilities of keeping the theme settings for a returning user, or respecting their browser settings by default if we’re working with new users.

Next, we add a tiny bit of content to the App component so we have some elements to style, along with a button and function to actually allow us to toggle the theme.

The finished App.tsx file

The secret sauce is on line 14 where we’ve added data-theme={theme} to our top-level <div>. Now, by switching the value of theme, we are choosing whether or not to override the CSS variables in :root with the ones in the data-theme='dark' section of the index.css file.

The last thing we need to do is add some styling that uses those CSS variables we made earlier, and it’ll up and running! Open App.css and drop this CSS in there:

.App {   color: var(--text-primary);   background-color: var(--background);   font-size: large;   font-weight: bold;   padding: 20px;   height: calc(100vh - 40px);   transition: all .5s; } button {   color: var(--text-primary);   background-color: var(--background);   border: 2px var(--text-primary) solid;   float: right;   transition: all .5s; }

Now the background and text for the main <div>, and the background, text, and outline of the <button> rely on the CSS variables. That means when the theme changes, everything that depends on those variables update as well. Also note that we added transition: all .5s to both the App and <button> for a smooth transition between color themes.

Now, head back to the browser that’s running the app, and here’s what we get:

Tada! Let’s add another component just to show how the system works if we’re building out a real app. We’ll add a /components folder in /src, put a /square folder in /components, and add a Square.tsx and square.css, like so:

Let’s import it back into App.tsx, like so:

Here’s what we have now as a result:

And there we go! Obviously, this is a pretty basic case where we’re only using a default (light) theme, and a secondary (dark) theme. But if your application calls for it, this system could be used to implement multiple theme options. Personally, I’m thinking of giving my next project options for light, dark, chocolate, and strawberry—go nuts!

Bonus: Integrating with React Scoped CSS:

Using React Scoped CSS is my favorite way to keep each component’s CSS encapsulated to prevent name collision messiness and unintended style inheritance. My previous go-to for this was CSS Modules, but that has the downside of making the in-browser DOM look like a robot wrote all of the class names… because that’s exactly the case. This lack of human-readability makes debugging far more annoying than it has to be. Enter React Scoped CSS. We get to keep writing CSS (or Sass) exactly the way we have been, and the output looks like a human wrote it.

Seeing as the the React Scoped CSS repo provides full and detailed installation instructions, I’ll merely summarize them here.

First, install and configure Create React App Configuration Override (CRACO) according to their instructions. Craco is a tool that lets us override some of the default webpack configuration that’s bundled into create-react-app (CRA). Normally, if you want to adjust webpack in a CRA project, you first have to “eject” the project, which is an irreversible operation, and makes you fully responsible for all of the dependencies that are normally handled for you. You usually want to avoid ejecting unless you really, really know what you’re doing and have a good reason to go down that road. Instead, CRACO let’s us make some minor adjustments to our webpack config without things getting messy.

Once that’s done, install the React Scoped CSS package:

npm i craco-plugin-scoped-css

(The README instructions use yarn for installation instead of npm, but either is fine.) Now that it’s installed, simply rename the CSS files by adding .scoped before the .css, like so:

app.css -> app.scoped.css

And we need to make sure we’re using a new name when importing that CSS into a component:

import './app.css'; -> import './app.scoped.css';

Now all of the CSS is encapsulated so that it only applies to the components they’re imported into. It works by using data-* properties, much like our theme system, so when a scoped CSS file is imported into a component, all of that component’s elements are labeled with a property, like data-v-46ef2374, and the styles from that file are wrapped so that they only apply to elements with that exact data property.

That’s all wonderful, but the little trick to making that work with this theming system is that we explicitly don’t want the CSS variables encapsulated; we want them applied to the whole project. So, we simply don’t change index.css to have scoped in it… in other words, we can leave that CSS file alone. That’s it! Now we have a powerful theming system working in harmony with scoped CSS— we’re living the dream!

Thank you so much taking a read through this guide, and if it helped you build something awesome, I would love to know about it!


The post Easy Dark Mode (and Multiple Color Themes!) in React appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

Implementing a single GraphQL across multiple data sources

(This is a sponsored post.)

In this article, we will discuss how we can apply schema stitching across multiple Fauna instances. We will also discuss how to combine other GraphQL services and data sources with Fauna in one graph.

What is Schema Stitching?

Schema stitching is the process of creating a single GraphQL API from multiple underlying GraphQL APIs.

Where is it useful?

While building large-scale applications, we often break down various functionalities and business logic into micro-services. It ensures the separation of concerns. However, there will be a time when our client applications need to query data from multiple sources. The best practice is to expose one unified graph to all your client applications. However, this could be challenging as we do not want to end up with a tightly coupled, monolithic GraphQL server. If you are using Fauna, each database has its own native GraphQL. Ideally, we would want to leverage Fauna’s native GraphQL as much as possible and avoid writing application layer code. However, if we are using multiple databases our front-end application will have to connect to multiple GraphQL instances. Such arrangement creates tight coupling. We want to avoid this in favor of one unified GraphQL server.

To remedy these problems, we can use schema stitching. Schema stitching will allow us to combine multiple GraphQL services into one unified schema. In this article, we will discuss

  1. Combining multiple Fauna instances into one GraphQL service
  2. Combining Fauna with other GraphQL APIs and data sources
  3. How to build a serverless GraphQL gateway with AWS Lambda?

Combining multiple Fauna instances into one GraphQL service

First, let’s take a look at how we can combine multiple Fauna instances into one GraphQL service. Imagine we have three Fauna database instances ProductInventory, and Review. Each is independent of the other. Each has its graph (we will refer to them as subgraphs). We want to create a unified graph interface and expose it to the client applications. Clients will be able to query any combination of the downstream data sources.

We will call the unified graph to interface our gateway service. Let’s go ahead and write this service.

We’ll start with a fresh node project. We will create a new folder. Then navigate inside it and initiate a new node app with the following commands.

mkdir my-gateway  cd my-gateway npm init --yes

Next, we will create a simple express GraphQL server. So let’s go ahead and install the express and express-graphqlpackage with the following command.

npm i express express-graphql graphql --save

Creating the gateway server

We will create a file called gateway.js . This is our main entry point to the application. We will start by creating a very simple GraphQL server.

const express = require('express'); const { graphqlHTTP } = require('express-graphql'); const { buildSchema }  = require('graphql');  // Construct a schema, using GraphQL schema language const schema = buildSchema(`   type Query {     hello: String   } `);  // The root provides a resolver function for each API endpoint const rootValue = {     hello: () => 'Hello world!', };  const app = express();  app.use(   '/graphql',   graphqlHTTP((req) => ({     schema,     rootValue,     graphiql: true,   })), );  app.listen(4000); console.log('Running a GraphQL API server at <http://localhost:4000/graphql>');

In the code above we created a bare-bone express-graphql server with a sample query and a resolver. Let’s test our app by running the following command.

node gateway.js

Navigate to [<http://localhost:4000/graphql>](<http://localhost:4000/graphql>) and you will be able to interact with the GraphQL playground.

Creating Fauna instances

Next, we will create three Fauna databases. Each of them will act as a GraphQL service. Let’s head over to fauna.com and create our databases. I will name them ProductInventory and Review

Once the databases are created we will generate admin keys for them. These keys are required to connect to our GraphQL APIs.

Let’s create three distinct GraphQL schemas and upload them to the respective databases. Here’s how our schemas will look.

# Schema for Inventory database type Inventory {   name: String   description: String   sku: Float   availableLocation: [String] }
# Schema for Product database type Product {   name: String   description: String   price: Float }
# Schema for Review database type Review {   email: String   comment: String   rating: Float }

Head over to the relative databases, select GraphQL from the sidebar and import the schemas for each database.

Now we have three GraphQL services running on Fauna. We can go ahead and interact with these services through the GraphQL playground inside Fauna. Feel free to enter some dummy data if you are following along. It will come in handy later while querying multiple data sources.

Setting up the gateway service

Next, we will combine these into one graph with schema stitching. To do so we need a gateway server. Let’s create a new file gateway.js. We will be using a couple of libraries from graphql tools to stitch the graphs.

Let’s go ahead and install these dependencies on our gateway server.

npm i @graphql-tools/schema @graphql-tools/stitch @graphql-tools/wrap cross-fetch --save 

In our gateway, we are going to create a new generic function called makeRemoteExecutor. This function is a factory function that returns another function. The returned asynchronous function will make the GraphQL query API call.

// gateway.js  const express = require('express'); const { graphqlHTTP } = require('express-graphql'); const { buildSchema }  = require('graphql');   function makeRemoteExecutor(url, token) {     return async ({ document, variables }) => {       const query = print(document);       const fetchResult = await fetch(url, {         method: 'POST',         headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },         body: JSON.stringify({ query, variables }),       });       return fetchResult.json();     }  }  // Construct a schema, using GraphQL schema language const schema = buildSchema(`   type Query {     hello: String   } `);  // The root provides a resolver function for each API endpoint const rootValue = {     hello: () => 'Hello world!', };  const app = express();  app.use(   '/graphql',   graphqlHTTP(async (req) => {     return {       schema,       rootValue,       graphiql: true,     }   }), );  app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql');

As you can see above the makeRemoteExecutor has two parsed arguments. The url argument specifies the remote GraphQL url and the token argument specifies the authorization token.

We will create another function called makeGatewaySchema. In this function, we will make the proxy calls to the remote GraphQL APIs using the previously created makeRemoteExecutor function.

// gateway.js  const express = require('express'); const { graphqlHTTP } = require('express-graphql'); const { introspectSchema } = require('@graphql-tools/wrap'); const { stitchSchemas } = require('@graphql-tools/stitch'); const { fetch } = require('cross-fetch'); const { print } = require('graphql');  function makeRemoteExecutor(url, token) {   return async ({ document, variables }) => {     const query = print(document);     const fetchResult = await fetch(url, {       method: 'POST',       headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },       body: JSON.stringify({ query, variables }),     });     return fetchResult.json();   } }  async function makeGatewaySchema() {      const reviewExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQZPUejACQ2xuvfi50APAJ397hlGrTjhdXVta');     const productExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQbI02HACQwTaUF9iOBbGC3fatQtclCOxZNfp');     const inventoryExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQbI02HACQwTaUF9iOBbGC3fatQtclCOxZNfp');      return stitchSchemas({         subschemas: [           {             schema: await introspectSchema(reviewExecutor),             executor: reviewExecutor,           },           {             schema: await introspectSchema(productExecutor),             executor: productExecutor           },           {             schema: await introspectSchema(inventoryExecutor),             executor: inventoryExecutor           }         ],                  typeDefs: 'type Query { heartbeat: String! }',         resolvers: {           Query: {             heartbeat: () => 'OK'           }         }     }); }  // ...

We are using the makeRemoteExecutor function to make our remote GraphQL executors. We have three remote executors here one pointing to Product , Inventory , and Review services. As this is a demo application I have hardcoded the admin API key from Fauna directly in the code. Avoid doing this in a real application. These secrets should not be exposed in code at any time. Please use environment variables or secret managers to pull these values on runtime.

As you can see from the highlighted code above we are returning the output of the switchSchemas function from @graphql-tools. The function has an argument property called subschemas. In this property, we can pass in an array of all the subgraphs we want to fetch and combine. We are also using a function called introspectSchema from graphql-tools. This function is responsible for transforming the request from the gateway and making the proxy API request to the downstream services.

You can learn more about these functions on the graphql-tools documentation site.

Finally, we need to call the makeGatewaySchema. We can remove the previously hardcoded schema from our code and replace it with the stitched schema.

// gateway.js  // ...  const app = express();  app.use(   '/graphql',   graphqlHTTP(async (req) => {     const schema = await makeGatewaySchema();     return {       schema,       context: { authHeader: req.headers.authorization },       graphiql: true,     }   }), );  // ...

When we restart our server and go back to localhost we will see that queries and mutations from all Fauna instances are available in our GraphQL playground.

Let’s write a simple query that will fetch data from all Fauna instances simultaneously.

Stitch third party GraphQL APIs

We can stitch third-party GraphQL APIs into our gateway as well. For this demo, we are going to stitch the SpaceX open GraphQL API with our services.

The process is the same as above. We create a new executor and add it to our sub-graph array.

// ...  async function makeGatewaySchema() {    const reviewExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQdRZVpACRMEEM1GKKYQxH2Qa4TzLKusTW2gN');   const productExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQdSdXiACRGmgJgAEgmF_ZfO7iobiXGVP2NzT');   const inventoryExecutor = await makeRemoteExecutor('https://graphql.fauna.com/graphql', 'fnAEQdR0kYACRWKJJUUwWIYoZuD6cJDTvXI0_Y70');    const spacexExecutor = await makeRemoteExecutor('https://api.spacex.land/graphql/')    return stitchSchemas({     subschemas: [       {         schema: await introspectSchema(reviewExecutor),         executor: reviewExecutor,       },       {         schema: await introspectSchema(productExecutor),         executor: productExecutor       },       {         schema: await introspectSchema(inventoryExecutor),         executor: inventoryExecutor       },       {         schema: await introspectSchema(spacexExecutor),         executor: spacexExecutor       }     ],              typeDefs: 'type Query { heartbeat: String! }',     resolvers: {       Query: {         heartbeat: () => 'OK'       }     }   }); }  // ...

Deploying the gateway

To make this a true serverless solution we should deploy our gateway to a serverless function. For this demo, I am going to deploy the gateway into an AWS lambda function. Netlify and Vercel are the two other alternatives to AWS Lambda.

I am going to use the serverless framework to deploy the code to AWS. Let’s install the dependencies for it.

npm i -g serverless # if you don't have the serverless framework installed already npm i serverless-http body-parser --save  

Next, we need to make a configuration file called serverless.yaml

# serverless.yaml  service: my-graphql-gateway  provider:   name: aws   runtime: nodejs14.x   stage: dev   region: us-east-1  functions:   app:     handler: gateway.handler     events:       - http: ANY /       - http: 'ANY {proxy+}'

Inside the serverless.yaml we define information such as cloud provider, runtime, and the path to our lambda function. Feel free to take look at the official documentation for the serverless framework for more in-depth information.

We will need to make some minor changes to our code before we can deploy it to AWS.

npm i -g serverless # if you don't have the serverless framework installed already npm i serverless-http body-parser --save 

Notice the highlighted code above. We added the body-parser library to parse JSON body. We have also added the serverless-http library. Wrapping the express app instance with the serverless function will take care of all the underlying lambda configuration.

We can run the following command to deploy this to AWS Lambda.

serverless deploy

This will take a minute or two to deploy. Once the deployment is complete we will see the API URL in our terminal.

Make sure you put /graphql at the end of the generated URL. (i.e. https://gy06ffhe00.execute-api.us-east-1.amazonaws.com/dev/graphql).

There you have it. We have achieved complete serverless nirvana 😉. We are now running three Fauna instances independent of each other stitched together with a GraphQL gateway.

Feel free to check out the code for this article here.

Conclusion

Schema stitching is one of the most popular solutions to break down monoliths and achieve separation of concerns between data sources. However, there are other solutions such as Apollo Federation which pretty much works the same way. If you would like to see an article like this with Apollo Federation please let us know in the comment section. That’s it for today, see you next time.


The post Implementing a single GraphQL across multiple data sources appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

One Action, Multiple Terminal Windows Running Stuff

Many development environments require running things in a terminal window. npm run start, or whatever. I know my biggest project requires me to be running a big fancy Docker-based thing in one terminal, Ruby on Rails in another, and webpack in another. I’ve worked on other projects that require multiple terminal windows as well, and I don’t feel like I’m that unusual. I’ve heard from several others in this situation. It’s not a bad situation, it’s just a little cumbersome and annoying. I’ve got to remember all the commands and set up my command line app in a way that feels comfortable. For me, splitting panels is nicer than tabs, although tabs for separate projects seems OK.

I asked the question on Twitter, of course. I figured I’d compile the options here.

  • tmux was the most popular answer. I’m very sure I don’t understand all it can do, but I think I understand that it makes “fake” panes within one terminal session that emulates multiple panes. So, those multiple panes can be configured to open and run different commands simultaneously. I found this interesting because it came literally days later my CodePen co-founder let us all know the new dev environment he’s been working on will use tmux.
  • I was pointed to kitty by a fella who told me it feels like a grown-up tmux to him. It can be configured into layouts with commands that run at startup.
  • There are native apps for all the platforms that can run multiple panels.
    • macOS: I’ve long used iTerm which does split panels nicely. It can also remember window arrangements, which I’ve used, but I don’t see any built-in option for triggering commands in that arrangement. The native terminal can do tabs and splitting, too, but it feels very limited.
    • Linux: Terminator
    • Windows: The default terminal has panes.
  • There are npm things for running multiple scripts, like concurrently and npm-run-all, but (I think?) they are limited to running only npm scripts, rather than any terminal command. Maybe you can make npm scripts for those other commands? But even then, I don’t think you’d see the output in different panels, so it’s probably best for scripts that are run-and-done instead of run-forever.

Being a Mac guy, I was most interested in solutions that would work with iTerm since I’ve used that anyway. In lieu of a built-in iTerm solution, I did learn it was “scriptable.” Apparently, they are sunsetting AppleScript support in favor of Python but, hey, for now it seems to work fine.

It’s basically this:

The Code
tell application "iTerm" 	   tell current window 		     create window with default profile     tell current session of current tab       set name to "run.sh"       write text "cd '/Users/chriscoyier/GitHub/CPOR'"       write text "./run.sh"     end tell 		     create tab with default profile     tell current session of current tab       set name to "Rails"       write text "cd '/Users/chriscoyier/GitHub/CPOR'"       write text "nvm use"       write text "yarn"       write text "bundle install"       write text "yarn run rails"     end tell 		     create tab with default profile     tell current session of current tab       set name to "webpack"       write text "cd '/Users/chriscoyier/GitHub/CPOR'"       write text "nvm use"       write text "yarn"       write text "yarn run dev"     end tell 		     # split vertically     # tell application "System Events" to keystroke "d" using command down     # delay 1 		     # split horizontally     # tell application "System Events" to keystroke "d" using {shift down, command down}     # delay 1 		     # moving... (requires permission)     # tell application "System Events" to keystroke "]" using command down 		     end tell 	 end tell

I just open that script, hit run, and it does the job. I left the comments in there because I’d like to figure out how to get it to do split screen the way I like, rather than tabs, but I got this working and then got lazy again. It felt weird to have to use keystrokes to have to do it, so I figured if I was going to dig in, I’d figure out if their newer Python stuff supports it more directly or what. It’s also funny I can’t like compile it into a little mini app or something. Can’t Automator do that? Shrug.

The other popular answer I got for Mac folks is that they have Alfred do the work. I never got into Alfred, but there clearly is fancy stuff you can do with it.


The post One Action, Multiple Terminal Windows Running Stuff appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

Radio Buttons Are Like Selects; Checkboxes Are Like Multiple Selects

I was reading Anna Kaley’s “Listboxes vs. Dropdown Lists” post the other day. It’s a fairly straightforward comparison between different UI implementations of selecting options. There is lots of good advice there. Classics like that you should use radio buttons (single select) or checkboxes (multiple select) if you’re showing five or fewer options, and the different options when the number of options grows from there.

One thing that isn’t talked about is how you implement these things. I imagine that’s somewhat on purpose as the point is to talk UX, not tech. But how you implement them plays a huge part in UX. In web design and development circles, the conversation about these things usually involves whether you can pull these things off with native controls, or if you need to rebuild them from scratch. If you can use native controls, you often should, because there are tons of UX that you get for free that that might otherwise be lost or forgotten when you rebuild — like how everything works via the keyboard.

The reason people chose “rebuild” is often for styling reasons, but that’s changing slowly over time. We’ve got lots of control over radios and checkboxes now. We can style the outside of selects pretty well and even the inside with trickery.

But even without custom styling, we still have some UI options. If you need to select one option from many, we’ve got <input type="radio"> buttons, but data and end-result-wise, that’s the same as a <select>. If you need to select multiple options, we’ve got <input type="checkbox">, but that’s data and end-result-wise the same as <select multiple>.

You pick based on the room you have available and the UX of whatever you’re building.

The post Radio Buttons Are Like Selects; Checkboxes Are Like Multiple Selects appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

CSS :not() with Multiple Classes

Say you want to select an element when it doesn’t have a certain class. That’s what the :not() selector is for.

body:not(.home) {    }

But what if there are multiple classes you want to avoid?

There are no logical combinators with :not(), like and or or, but you can chain them, which is effectively like and.

body:not(.home):not(.away):not(.page-50) {    }

The :not() selector doesn’t add any specificy by itself, but what is inside does, so :not(.foo) adds the same weight as .foo does.

The post CSS :not() with Multiple Classes appeared first on CSS-Tricks.

CSS-Tricks

,
[Top]

Managing Multiple Backgrounds with Custom Properties

One cool thing about CSS custom properties is that they can be a part of a value. Let’s say you’re using multiple backgrounds to pull off a a design. Each background will have its own color, image, repeat, position, etc. It can be verbose!

You have four images:

body {      background-position:     top 10px left 10px,     top 10px right 10px,     bottom 10px right 10px,     bottom 10px left 10px;      background-repeat: no-repeat;      background-image:     url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-left.svg),     url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-right.svg),     url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-right.svg),     url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg);    }

You want to add a fifth in a media query:

@media (min-width: 1500px) {   body {     /* REPEAT all existing backgrounds, then add a fifth. */   } }

That’s going to be super verbose! You’ll have to repeat each of those four images again, then add the fifth. Lots of duplication there.

One possibility is to create a variable for the base set, then add the fifth much more cleanly:

body {   --baseBackgrounds:      url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-left.svg),     url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-right.svg),     url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-right.svg),     url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg);    background-position:     top 10px left 10px,     top 10px right 10px,     bottom 10px right 10px,     bottom 10px left 10px;      background-repeat: no-repeat;      background-image: var(--baseBackgrounds); } @media (min-width: 1500px) {   body {     background-image:        var(--baseBackgrounds),       url(added-fifth-background.svg);   } }

But, it’s really up to you. It might make more sense and be easier manage if you made each background image into a variable, and then pieced them together as needed.

body {   --bg1: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-left.svg);   --bg2: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-right.svg);   --bg3: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-right.svg);   --bg4: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg);   --bg5: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg);      background-image: var(--bg1), var(--bg2), var(--bg3), var(--bg4); } @media (min-width: 1500px) {   body {     background-image: var(--bg1), var(--bg2), var(--bg3), var(--bg4), var(--bg5);   } }

Here’s a basic version of that, including a supports query:

See the Pen
Multiple BGs with Custom Properties
by Chris Coyier (@chriscoyier)
on CodePen.

Dynamically changing just the part of a value is a huge strength of CSS custom properties!

Note, too, that with backgrounds, it might be best to include the entire shorthand as the variable. That way, it’s much easier to piece everything together at once, rather than needing something like…

--bg_1_url: url(); --bg_1_size: 100px; --bg_1_repeat: no-repeat; /* etc. */

It’s easier to put all of the properties into shorthand and use as needed:

body {     --bg_1: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-left.svg) top 10px left 10px / 86px no-repeat;   --bg_2: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-top-right.svg) top 10px right 10px / 86px no-repeat;   --bg_3: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-right.svg) bottom 10px right 10px / 86px no-repeat;   --bg_4: url(https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/angles-bottom-left.svg) bottom 10px left 10px  / 86px no-repeat;        background:     var(--bg_1), var(--bg_2),var(--bg_3),var(--bg_4); }

Like this.

The post Managing Multiple Backgrounds with Custom Properties appeared first on CSS-Tricks.

CSS-Tricks

, , , ,
[Top]

CSS Triangles, Multiple Ways

I like Adam Laki’s Quick Tip: CSS Triangles because it covers that ubiquitous fact about front-end techniques: there are always many ways to do the same thing. In this case, drawing a triangle can be done:

  • with border and a collapsed element
  • with clip-path: polygon()
  • with transform: rotate() and overflow: hidden
  • with glyphs like ▼

I’d say that the way I’ve typically done triangles the most over the years is with the border trick, but I think my favorite way now is using clip-path. Code like this is fairly clear, understandable, and maintainable to me: clip-path: polygon(50% 0, 0 100%, 100% 100%); Brain: Middle top! Bottom right! Bottom left! Triangle!

My 2nd Place method goes to an option that didn’t make Adam’s list: inline <svg>! This kind of thing is nearly just as brain-friendly: <polygon points="0,0 100,0 50,100"/>.

Direct Link to ArticlePermalink

The post CSS Triangles, Multiple Ways appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

Multiple Background Clip

You know how you can have multiple backgrounds?

body {   background-image:      url(image-one.jpg),     url(image-two.jpg); }

That’s just background-image. You can set their position too, as you might expect. We’ll shorthand it:

body {   background:      url(image-one.jpg) no-repeat top right,     url(image-two.jpg) no-repeat bottom left; }

I snuck background-repeat in there just for fun. Another one you might not think of setting for multiple different backgrounds, though, is background-clip. In this linked article, Stefan Judis notes that this unlocks some pretty legit CSS-Trickery!

Direct Link to ArticlePermalink

The post Multiple Background Clip appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]