The Story Behind TryShape, a Showcase for the CSS clip-path property

I love shapes, especially colorful ones! Shapes on websites are in the same category of helpfulness as background colors, images, banners, section separators, artwork, and many more: they can help us understand context and inform our actions through affordances.

A few months back, I built an application to engage my 7-year old daughter with mathematics. Apart from basic addition and subtraction, my aim was to present questions using shapes. That’s when I got familiar with the CSS clip-path property, a reliable way to make shapes on the web. Then, I ended up building another app called, TryShape using the power of clip-path.

I’ll walk you through the story behind TryShape and how it helps create, manage, share, and export shapes. We’ll cover a lot about CSS clip-path along the way and how it helped me quickly build the app.

Here are a few important links:

First, the CSS clip-path property and shapes

Imagine you have a plain piece of paper and a pencil to draw a shape (say, a square) on it. How will you proceed? Most likely, you will start from a point, then draw a line to reach another point, then repeat it exact three more times to come back to the initial point. You also have to make sure you have opposite lines parallel and of the same length.

So, the essential ingredients for a shape are points, lines, directions, curves, angles, and lengths, among many others. The CSS clip-path helps specify many of these properties to clip a region of an HTML element to show a specific region. The part that is inside the clipped region is shown, and the rest is hidden. It gives an ocean of opportunities to developers to create various shapes using clip-path property.

Learn more about clipping and how it is different from masking.

The clip-path values for shape creation

The clip-path property accepts the following values for creating shapes:

  • circle()
  • ellipse()
  • inset()
  • polygon()
  • A clip source using url() function
  • path()

We need to understand the basic coordinate system a bit to use these values. When applying the clip-path property on an element to create shapes, we must consider the x-axis, y-axis, and the initial coordinates (0,0) at the element’s top-left corner.

Here is a div element with its x-axis, y-axis, and initial coordinates (0,0).

A blue square with its starting point located at a zero zero position on a chart with x and y axes.
Initial coordinates(0,0) with x-axis and y-axis

Now let’s use the circle() value to create a circular shape. We can specify the position and radius of the circle using this value. For example, to clip a circular shape at the coordinate position (70, 70) with a radius of 70px, we can specify the clip-path property value as:

clip-path: circle(70px at 70px 70px)

So, the center of the circle is placed at the coordinate (70, 70) with a 70px radius. Now, only this circular region is clipped and shown on the element. The rest of the portion of the element is hidden to create the impression of a circle shape.

A blue circle on a grid with an x and y axis. The circle starts at the zero zero position and its center is located at 70 px and 70px. A zoomed in area shows the clipping path, which is also located at 70x and 70px.
The center of the circle is placed at (70, 70) coordinates with a 70px x 70px area clipped. Hence the full circle is shown.

Next, what if we want to specify the position at (0,0)? In this case, the circle’s center is placed at the (0,0) position with a radius of 70px. That makes only a portion of the circle visible inside the element.

The center of a blue circle is placed at the 0,0 coordinates with a 70px by 70px area clipping the bottom-left region of the circle.
The center of the circle is placed at (0, 0) coordinates with a 70px x 70px area clipping the bottom-left region of the circle.

Let’s move on to use the other two essential values, inset() and polygon(). We use an inset to define a rectangular shape. We can specify the gap that each of the four edges may have to clip a region from an element. For example:

clip-path: inset(30px)

The above clip-path value clips a region by leaving out the 30px values from element’s edges. We can see that in the image below. We can also specify a different inset value for each of the edges.

The inset() function allows us to clip and area from the outside edge of a shape.

Next is the polygon() value. We can create a polygonal shape using a set of vertices. Take this example:

clip-path: polygon(10% 10%, 90% 10%, 90% 90%, 10% 80%)

Here we are specifying a set of vertices to create a region for clipping. The image below shows the position of each vertex to create a polygonal shape. We can specify as many vertices as we want.

A square with four points inside of it located at 10% by 10%, 90% by 10%, 10% by 80% and 90% by 90%, creating the clipped area.
The polygon() function allows us to create polygonal shapes using the set of vertices passed to it.

Next, let’s take a look at the ellipse() and the url() values. The ellipse() value helps create shapes by specifying two radii values and a position. In the image below, we see an ellipse at the position where the radii is at (50%,50%) and the shape is 70px wide and 100px tall.

A blue blue against a light blue background. The ellipse is 70 pixels wide and 100 pixels tall and it's radius is centered at 50% 50%.
We need to specify two radii values and a position to create an ellipse.

url() is a CSS function to specify the clip-path element’s ID value to render an SVG shape. Please take a look at the image below. We have defined a SVG shape using clipPath and path elements. You can use the ID value of the clipPath element as an argument to the url() function to render this shape.

Showing the SVG code for a heart-shaped path and the actual heart next to it in blue.
Here, we are creating a heart shape using the url() function

Additionally, we can use the path values directly in the path() function to draw the shape.

Showing a line of CSS code for the clip-path property filled in with the code of an SVG path inside the path function. The result is below the code, a parabolic curve that's filled in blue.
Here we are creating a curvy shape using the path() function.

Alright. I hope you have got an understanding of different clip-path property values. With this understanding, let’s take a loot at some implementations and play around with them. Here is a Pen for you. Please use it to try adding, modifying values to create a new shape.

Let’s talk about TryShape

It’s time to talk about TryShape and its background story. TryShape is an open-source application that helps create, export, share, and use any shapes of your choice. You can create banners, circles, arts, polygons and export them as SVG, PNG, and JPEG files. You can also create a CSS code snippet to copy and use in your application.

TryShape is built using the following framework and libraries (and clip-path, of course):

  • CSS clip-path: We’ve already discussed the power of this awesome CSS property.
  • Next.js: The coolest React-based framework around. It helped me create pages, components, interactions, and APIs to connect to the back-end database.
  • HarperDB: A flexible database to store data and query them using both SQL and No-SQL interactions. TryShape has its schema and tables created in the HarperDB cloud. The Next.js APIs interact with the schema and tables to perform required CRUD operations from the user interface.
  • Firebase: Authentication services from Google. TryShape uses it to get the social login working using Google, GitHub, Twitter, and other accounts.
  • react-icons: One shop for all the icons for a React-based application
  • date-fns: The modern, lightweight library for date formatting
  • axios: Making the API calls easy from the React components
  • styled-components: A structured way to create CSS rules from react components
  • react-clip-path: A homegrown module to handle clip-path property in a React app
  • react-draggable: Make an HTML element draggable in a React app. TryShape uses it to adjust the position of shape vertices.
  • downloadjs: Trigger a download from JavaScript
  • html-to-image: Converts an HTML element to image (including SVG, JPEG, and PNG)
  • Vercel: Best for hosting a Next.js app

Creating shapes in TryShape using CSS clip-path

Let me highlight the source code that helps create a shape using the CSS clip-path property. The code snippet below defines the user interface structure for a container element (Box) that’s 300px square. The Box element has two child elements, Shadow and Component.

<Box 
  height="300px" 
  width="300px" 
  onClick={(e) => props.handleChange(e)}>
  { 
    props.shapeInformation.showShadow && 
    <Shadow 
      backgroundColor={props.shapeInformation.backgroundColor} 
      id="shapeShadow" /> 
  }
  <Component 
    formula={props.shapeInformation.formula} 
    backgroundColor={props.shapeInformation.backgroundColor} 
    id="clippedShape" />
</Box>

The Shadow component defines the area that is hidden by the clip-path clipping. We create it to show a light color background to make this area partially visible to the end user. The Component is to assign the clip-path value to show the clipped area.

See the styled-component definitions of Box, Shadow, and Component below:

// The styled-components code to create the UI components using CSS properties

// The container div
const Box = styled.div`
  width: ${props => props.width || '100px'};
  height: ${props => props.height || '100px'};
  margin: 0 auto;
  position: relative;
`;

// Shadow defines the area that is hidden by the `clip-path` clipping
// We show a light color background to make this area partially visible.
const Shadow = styled.div`
  background-color: ${props => props.backgroundColor || '#00c4ff'};
  opacity: 0.25;
  position: absolute;
  top: 10px;
  left: 10px;
  right: 10px;
  bottom: 10px;
`;

// The actual component that takes the `clip-path` value (formula) and set
// to the `clip-path` property.
const Component = styled.div`
  clip-path: ${props => props.formula}; // the formula is the clip-path value
  background-color: ${props => props.backgroundColor || '#00c4ff'};
  position: absolute;
  top: 10px;
  left: 10px;
  right: 10px;
  bottom: 10px;
`;
An illustration of a blue box with its inset clip points drawing another square that indicates the visible area of the shape in a darker blue.
The components to show a shape(both visible and hidden areas) after the clipping.

Please feel free to look into the entire codebase in the GitHub repo.

The future scope of TryShape

TryShape works well with the creation and management of basic shapes using CSS clip-path in the background. It is helpful to export the shapes and the CSS code snippets to use in your web applications. It has the potential to grow with many more valuable features. The primary one will be the ability to create shapes with curvy edges.

To support the curvy shapes, we need the support of the following values in TryShape:

  • a clip source using url() and
  • path().

With the help of these values, we can create shapes using SVG and then use one of the above values. Here is an example of the url() CSS function to create a shape using the SVG support.

<div class="heart">Heart</div>
<svg>
  <clipPath id="heart-path" clipPathUnits="objectBoundingBox">
    <path d="M0.5,1
      C 0.5,1,0,0.7,0,0.3
      A 0.25,0.25,1,1,1,0.5,0.3
      A 0.25,0.25,1,1,1,1,0.3
      C 1,0.7,0.5,1,0.5,1 Z" />
  </clipPath>
</svg>

Then, the CSS::

.heart {
  clip-path: url(#heart-path);
}
It produces a heart shape like this.

Now, let’s create a shape using the path() value. The HTML should have an element like a div:

<div class="curve">Curve</div>

In CSS:

.curve {
  clip-path: path("M 10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80");
}

Before we end…

I hope you enjoyed meeting my TryShape app and learning about the idea that leads to it, the strategies I considered, the technology under the hood, and its future potential. Please consider trying it and looking through the source code. And, of course, feel free to contribute to it with issues, feature requests, and code.

Before we end, I want to leave you with this short video prepared for the Hashnode hackathon where TryShape was an entry and finally in the list of winners. I hope you enjoy it.

Let’s connect. You can @ me on Twitter (@tapasadhikary) with comments, or feel free to follow.


The post The Story Behind TryShape, a Showcase for the CSS clip-path property appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

Theming and Theme Switching with React and styled-components

I recently had a project with a requirement to support theming on the website. It was a bit of a strange requirement, as the application is mostly used by a handful of administrators. An even bigger surprise was that they wanted not only to choose between pre-created themes, but build their own themes. I guess the people want what they want!

Let’s distill that into a complete list of more detailed requirements, then get it done!

  • Define a theme (i.e. background color, font color, buttons, links, etc.)
  • Create and save multiple themes
  • Select and apply a theme
  • Switch themes
  • Customize a theme

We delivered exactly that to our client, and the last I heard, they were using it happily!

Let’s get into building exactly that. We’re going to use React and styled-components. All the source code used in the article can be found in the GitHub Repository.

The setup

Let’s set up a project with React and styled-components. To do that, we will be using the create-react-app. It gives us the environment we need to develop and test React applications quickly.

Open a command prompt and use this command to create the project:

npx create-react-app theme-builder

The last argument, theme-builder, is just the name of the project (and thus, the folder name). You can use anything you like.

It may take a while. When done, navigate it to it in the command line with cd theme-builder. Open the file src/App.js file and replace the content with the following:

import React from 'react';

function App() {
  return (
    <h1>Theme Builder</h1>
  );
}

export default App;

This is a basic React component that we will modify soon. Run the following command from the project root folder to start the app:

# Or, npm run start
yarn start

You can now access the app using the URL http://localhost:3000.

A simple heading 1 that says Theme Builder in black on a white background.

create-react-app comes with the test file for the App component. As we will not be writing any tests for the components as part of this article, you can choose to delete that file.

We have to install a few dependencies for our app. So let’s install those while we’re at it:

# Or, npm i ...
yarn add styled-components webfontloader lodash

Here’s what we get:

  • styled-components: A flexible way to style React components with CSS. It provides out-of-the-box theming support using a wrapper component called, <ThemeProvider>. This component is responsible for providing the theme to all other React components that are wrapped within it. We will see this in action in a minute.
  • Web Font Loader: The Web Font Loader helps load fonts from various sources, like Google Fonts, Adobe Fonts, etc. We will use this library to load fonts when a theme is applied.
  • lodash: This is a JavaScript utility library for some handy little extras.

Define a theme

This is the first of our requirements. A theme should have a certain structure to define appearance, including colors, fonts, etc. For our application, we will define each theme with these properties:

  • unique identifier
  • theme name
  • color definitions
  • fonts
Screenshot of a code editor showing the organized structure of properties for a sea wave theme.
A theme is a structured group of properties that we’ll use in the application.

You may have more properties and/or different ways to structure it, but these are the things we’re going to use for our example.

Create and save multiple themes

So, we just saw how to define a theme. Now let’s create multiple themes by adding a folder in the project at src/theme and a file in it called, schema.json. Here’s what we can drop in that file to establish “light” and “sea wave” themes:

{
  "data" : {
    "light" : {
      "id": "T_001",
      "name": "Light",
      "colors": {
        "body": "#FFFFFF",
        "text": "#000000",
        "button": {
          "text": "#FFFFFF",
          "background": "#000000"
        },
        "link": {
          "text": "teal",
          "opacity": 1
        }
      },
      "font": "Tinos"
    },
    "seaWave" : {
      "id": "T_007",
      "name": "Sea Wave",
      "colors": {
        "body": "#9be7ff",
        "text": "#0d47a1",
        "button": {
          "text": "#ffffff",
          "background": "#0d47a1"
        },
        "link": {
          "text": "#0d47a1",
          "opacity": 0.8
        }
      },
      "font": "Ubuntu"
    }
  }
}

The content of the schema.json file can be saved to a database so we can persist all the themes along with the theme selection. For now, we will simply store it in the browser’s localStorage. To do that, we’ll create another folder at src/utils with a new file in it called, storage.js. We only need a few lines of code in there to set up localStorage:

export const setToLS = (key, value) => {
  window.localStorage.setItem(key, JSON.stringify(value));
}

export const getFromLS = key => {
  const value = window.localStorage.getItem(key);

  if (value) {
    return JSON.parse(value);
  }
}

These are simple utility functions to store data to the browser’s localStorage and to retrieve from there. Now we will load the themes into the browser’s localStorage when the app comes up for the first time. To do that, open the index.js file and replace the content with the following,

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import * as themes from './theme/schema.json';
import { setToLS } from './utils/storage';

const Index = () => {
  setToLS('all-themes', themes.default);
  return(
    <App />
  )
}

ReactDOM.render(
  <Index />
  document.getElementById('root')
);

Here, we are getting the theme information from the schema.json file and adding it to the localStorage using the key all-themes. If you have stopped the app running, please start it again and access the UI. You can use DevTools in the browser to see the themes are loaded into localStorage.

The theme with DevTools open and showing the theme properties in the console.
All of the theme props are properly stored in the browser’s localStorage, as seen in DevTools, under Application → Local Storage.

Select and apply a theme

We can now use the theme structure and supply the theme object to the <ThemeProvider> wrapper.

First, we will create a custom React hook. This will manage the selected theme, knowing if a theme is loaded correctly or has any issues. Let’s start with a new useTheme.js file inside the src/theme folder with this in it:

import { useEffect, useState } from 'react';
import { setToLS, getFromLS } from '../utils/storage';
import _ from 'lodash';

export const useTheme = () => {
  const themes = getFromLS('all-themes');
  const [theme, setTheme] = useState(themes.data.light);
  const [themeLoaded, setThemeLoaded] = useState(false);

  const setMode = mode => {
    setToLS('theme', mode)
    setTheme(mode);
  };

  const getFonts = () => {
    const allFonts = _.values(_.mapValues(themes.data, 'font'));
    return allFonts;
  }

  useEffect(() =>{
    const localTheme = getFromLS('theme');
    localTheme ? setTheme(localTheme) : setTheme(themes.data.light);
    setThemeLoaded(true);
  }, []);

  return { theme, themeLoaded, setMode, getFonts };
};

This custom React hook returns the selected theme from localStorage and a boolean to indicate if the theme is loaded correctly from storage. It also exposes a function, setMode, to apply a theme programmatically. We will come back to that in a bit. With this, we also get a list of fonts that we can load later using a web font loader.

It would be a good idea to use global styles to control things, like the site’s background color, font, button, etc. styled-components provides a component called, createGlobalStyle that establishes theme-aware global components. Let’s set those up in a file called, GlobalStyles.js in the src/theme folder with the following code:

import { createGlobalStyle} from "styled-components";

export const GlobalStyles = createGlobalStyle`
  body {
    background: ${({ theme }) => theme.colors.body};
    color: ${({ theme }) => theme.colors.text};
    font-family: ${({ theme }) => theme.font};
    transition: all 0.50s linear;
  }

  a {
    color: ${({ theme }) => theme.colors.link.text};
    cursor: pointer;
  }

  button {
    border: 0;
    display: inline-block;
    padding: 12px 24px;
    font-size: 14px;
    border-radius: 4px;
    margin-top: 5px;
    cursor: pointer;
    background-color: #1064EA;
    color: #FFFFFF;
    font-family: ${({ theme }) => theme.font};
  }

  button.btn {
    background-color: ${({ theme }) => theme.colors.button.background};
    color: ${({ theme }) => theme.colors.button.text};
  }
`;

Just some CSS for the <body>, links and buttons, right? We can use these in the App.js file to see the theme in action by replace the content in it with this:

// 1: Import
import React, { useState, useEffect } from 'react';
import styled, { ThemeProvider } from "styled-components";
import WebFont from 'webfontloader';
import { GlobalStyles } from './theme/GlobalStyles';
import {useTheme} from './theme/useTheme';

// 2: Create a cotainer
const Container = styled.div`
  margin: 5px auto 5px auto;
`;

function App() {
  // 3: Get the selected theme, font list, etc.
  const {theme, themeLoaded, getFonts} = useTheme();
  const [selectedTheme, setSelectedTheme] = useState(theme);

  useEffect(() => {
    setSelectedTheme(theme);
   }, [themeLoaded]);

  // 4: Load all the fonts
  useEffect(() => {
    WebFont.load({
      google: {
        families: getFonts()
      }
    });
  });

  // 5: Render if the theme is loaded.
  return (
    <>
    {
      themeLoaded && <ThemeProvider theme={ selectedTheme }>
        <GlobalStyles/>
        <Container style={{fontFamily: selectedTheme.font}}>
          <h1>Theme Builder</h1>
          <p>
            This is a theming system with a Theme Switcher and Theme Builder.
            Do you want to see the source code? <a href="https://github.com/atapas/theme-builder" target="_blank">Click here.</a>
          </p>
        </Container>
      </ThemeProvider>
    }
    </>
  );
}

export default App;

A few things are happening here:

  1. We import the useState and useEffect React hooks which will help us to keep track of any of the state variables and their changes due to any side effects. We import ThemeProvider and styled from styled-components. The WebFont is also imported to load fonts. We also import the custom theme, useTheme, and the global style component, GlobalStyles.
  2. We create a Container component using the CSS styles and styled component.
  3. We declare the state variables and look out for the changes.
  4. We load all the fonts that are required by the app.
  5. We render a bunch of text and a link. But notice that we are wrapping the entire content with the <ThemeProvider> wrapper which takes the selected theme as a prop. We also pass in the <GlobalStyles/> component.

Refresh the app and we should see the default “light” theme enabled.

The theme with a white background and black text.
Hey, look at that clean, stark design!

We should probably see if switching themes works. So, let’s open the useTheme.js file and change this line:

localTheme ? setTheme(localTheme) : setTheme(themes.data.light);

…to:

localTheme ? setTheme(localTheme) : setTheme(themes.data.seaWave);

Refresh the app again and hopefully we see the “sea wave” theme in action.

The same theme in with a blue color scheme with a light blue background and dark blue text and a blue button.
Now we’re riding the waves of this blue-dominant theme.

Switch themes

Great! We are able to correctly apply themes. How about creating a way to switch themes just with the click of a button? Of course we can do that! We can also provide some sort of theme preview as well.

A heading instructs the user to select a theme and two card components are beneath the heading, side-by-side, showing previews of the light theme and the sea wave theme.
A preview of each theme is provided in the list of options.

Let’s call each of these boxes a ThemeCard, and set them up in a way they can take its theme definition as a prop. We’ll go over all the themes, loop through them, and populate each one as a ThemeCard component.

{
  themes.length > 0 && 
  themes.map(theme =>(
    <ThemeCard theme={data[theme]} key={data[theme].id} />
  ))
}

Now let’s turn to the markup for a ThemeCard. Yours may look different, but notice how we extract its own color and font properties, then apply them:

const ThemeCard = props => {
  return(
    <Wrapper 
      style={{backgroundColor: `${data[_.camelCase(props.theme.name)].colors.body}`, color: `${data[_.camelCase(props.theme.name)].colors.text}`, fontFamily: `${data[_.camelCase(props.theme.name)].font}`}}>
      <span>Click on the button to set this theme</span>
      <ThemedButton
        onClick={ (theme) => themeSwitcher(props.theme) }
        style={{backgroundColor: `${data[_.camelCase(props.theme.name)].colors.button.background}`, color: `${data[_.camelCase(props.theme.name)].colors.button.text}`, fontFamily: `${data[_.camelCase(props.theme.name)].font}`}}>
        {props.theme.name}
      </ThemedButton>
    </Wrapper>
  )
}

Next up, let’s create a file called ThemeSelector.js in our the src folder. Copy the content from here and drop it into the file to establish our theme switcher, which we need to import in App.js:

import ThemeSelector from './ThemeSelector';

Now we can use it inside the Container component:

<Container style={{fontFamily: selectedTheme.font}}>
  // same as before
  <ThemeSelector setter={ setSelectedTheme } />
</Container>

Let’s refresh the browser now and see how switching themes works.

An animated screenshot showing the theme changing when it is selected from the list of theme card options.

The fun part is, you can add as many as themes in the schema.json file to load them in the UI and switch. Check out this schema.json file for some more themes. Please note, we are also saving the applied theme information in localStorage, so the selection will be retained when you reopen the app next time.

Selected theme stored in the Local Storage.

Customize a theme

Maybe your users like some aspects of one theme and some aspects of another. Why make them choose between them when they can give them the ability to define the theme props themselves! We can create a simple user interface that allows users to select the appearance options they want, and even save their preferences.

Animated screenshot showing a modal opening with a list of theme options to customize the appearance, including the them name, background color, text color, button text color, link color, and font.

We will not cover the theme creation code explanation in details but, it should be easy by following the code in the GitHub Repo. The main source file is CreateThemeContent.js and it is used by App.js. We create the new theme object by gathering the value from each input element change event and add the object to the collection of theme objects. That’s all.

Before we end…

Thank you for reading! I hope you find what we covered here useful for something you’re working on. Theming systems are fun! In fact, CSS custom properties are making that more and more a thing. For example, check out this approach for color from Dieter Raber and this roundup from Chris. There’s also this setup from Michelle Barker that relies on custom properties used with Tailwind CSS. Here’s yet another way from Andrés Galente.

Where all of these are great example for creating themes, I hope this article helps take that concept to the next level by storing properties, easily switching between themes, giving users a way to customize a theme, and saving those preferences.

Let’s connect! You can DM me on Twitter with comments, or feel free to follow.


The post Theming and Theme Switching with React and styled-components appeared first on CSS-Tricks.

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

How to create a client-serverless Jamstack app using Netlify, Gatsby and Fauna

The Jamstack is a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup.

The key aspects of a Jamstack application are the following:

  • The entire app runs on a CDN (or ADN). CDN stands for Content Delivery Network and an ADN is an Application Delivery Network.
  • Everything lives in Git.
  • Automated builds run with a workflow when developers push the code.
  • There’s Automatic deployment of the prebuilt markup to the CDN/ADN.
  • Reusable APIs make hasslefree integrations with many of the services. To take a few examples, Stripe for the payment and checkout, Mailgun for email services, etc. We can also write custom APIs targeted to a specific use-case. We will see such examples of custom APIs in this article.
  • It’s practically Serverless. To put it more clearly, we do not maintain any servers, rather make use of already existing services (like email, media, database, search, and so on) or serverless functions.

In this article, we will learn how to build a Jamstack application that has:

  • A global data store with GraphQL support to store and fetch data with ease. We will use Fauna to accomplish this.
  • Serverless functions that also act as the APIs to fetch data from the Fauna data store. We will use Netlify serverless functions for this.
  • We will build the client side of the app using a Static Site Generator called Gatsbyjs.
  • Finally we will deploy the app on a CDN configured and managed by Netlify CDN.

So, what are we building today?

We all love shopping. How cool would it be to manage all of our shopping notes in a centralized place? So we’ll be building an app called ‘shopnote’ that allows us to manage shop notes. We can also add one or more items to a note, mark them as done, mark them as urgent, etc.

At the end of this article, our shopnote app will look like this,

TL;DR

We will learn things with a step-by-step approach in this article. If you want to jump into the source code or demonstration sooner, here are links to them.

Set up Fauna

Fauna is the data API for client-serverless applications. If you are familiar with any traditional RDBMS, a major difference with Fauna would be, it is a relational NOSQL system that gives all the capabilities of the legacy RDBMS. It is very flexible without compromising scalability and performance.

Fauna supports multiple APIs for data-access,

  • GraphQL: An open source data query and manipulation language. If you are new to the GraphQL, you can find more details from here, https://graphql.org/
  • Fauna Query Language (FQL): An API for querying Fauna. FQL has language specific drivers which makes it flexible to use with languages like JavaScript, Java, Go, etc. Find more details of FQL from here.

In this article we will explain the usages of GraphQL for the ShopNote application.

First thing first, sign up using this URL. Please select the free plan which is with a generous daily usage quota and more than enough for our usage.

Next, create a database by providing a database name of your choice. I have used shopnotes as the database name.

After creating the database, we will be defining the GraphQL schema and importing it into the database. A GraphQL schema defines the structure of the data. It defines the data types and the relationship between them. With schema we can also specify what kind of queries are allowed.

At this stage, let us create our project folder. Create a project folder somewhere on your hard drive with the name, shopnote. Create a file with the name, shopnotes.gql with the following content:

type ShopNote {
  name: String!
  description: String
  updatedAt: Time
  items: [Item!] @relation
}
 
type Item {
  name: String!
  urgent: Boolean
  checked: Boolean
  note: ShopNote!
}
 
type Query {
  allShopNotes: [ShopNote!]!
}

Here we have defined the schema for a shopnote list and item, where each ShopNote contains name, description, update time and a list of Items. Each Item type has properties like, name, urgent, checked and which shopnote it belongs to. 

Note the @relation directive here. You can annotate a field with the @relation directive to mark it for participating in a bi-directional relationship with the target type. In this case, ShopNote and Item are in a one-to-many relationship. It means, one ShopNote can have multiple Items, where each Item can be related to a maximum of one ShopNote.

You can read more about the @relation directive from here. More on the GraphQL relations can be found from here.

As a next step, upload the shopnotes.gql file from the Fauna dashboard using the IMPORT SCHEMA button,

Upon importing a GraphQL Schema, FaunaDB will automatically create, maintain, and update, the following resources:

  • Collections for each non-native GraphQL Type; in this case, ShopNote and Item.
  • Basic CRUD Queries/Mutations for each Collection created by the Schema, e.g. createShopNote allShopNotes; each of which are powered by FQL.
  • For specific GraphQL directives: custom Indexes or FQL for establishing relationships (i.e. @relation), uniqueness (@unique), and more!

Behind the scene, Fauna will also help to create the documents automatically. We will see that in a while.

Fauna supports a schema-free object relational data model. A database in Fauna may contain a group of collections. A collection may contain one or more documents. Each of the data records are inserted into the document. This forms a hierarchy which can be visualized as:

Here the data record can be arrays, objects, or of any other supported types. With the Fauna data model we can create indexes, enforce constraints. Fauna indexes can combine data from multiple collections and are capable of performing computations. 

At this stage, Fauna already created a couple of collections for us, ShopNote and Item. As we start inserting records, we will see the Documents are also getting created. We will be able view and query the records and utilize the power of indexes. You may see the data model structure appearing in your Fauna dashboard like this in a while,

Point to note here, each of the documents is identified by the unique ref attribute. There is also a ts field which returns the timestamp of the recent modification to the document. The data record is part of the data field. This understanding is really important when you interact with collections, documents, records using FQL built-in functions. However, in this article we will interact with them using GraphQL queries with Netlify Functions.

With all these understanding, let us start using our Shopenotes database that is created successfully and ready for use. 

Let us try some queries

Even though we have imported the schema and underlying things are in place, we do not have a document yet. Let us create one. To do that, copy the following GraphQL mutation query to the left panel of the GraphQL playground screen and execute.

mutation {
  createShopNote(data: {
    name: "My Shopping List"
    description: "This is my today's list to buy from Tom's shop"
    items: {
      create: [
        { name: "Butther - 1 pk", urgent: true }
        { name: "Milk - 2 ltrs", urgent: false }
        { name: "Meat - 1lb", urgent: false }
      ]
    }
  }) {
    _id
    name
    description
    items {
      data {
        name,
        urgent
      }
    }
  }
}

Note, as Fauna already created the GraphQL mutation classes in the background, we can directly use it like, createShopNote. Once successfully executed, you can see the response of a ShopNote creation at the right side of the editor.

The newly created ShopNote document has all the required details we have passed while creating it. We have seen ShopNote has a one-to-many relation with Item. You can see the shopnote response has the item data nested within it. In this case, one shopnote has three items. This is really powerful. Once the schema and relation are defined, the document will be created automatically keeping that relation in mind.

Now, let us try fetching all the shopnotes. Here is the GraphQL query:

query {
    allShopNotes {
    data {
      _id
      name
      description
      updatedAt
      items {
        data {
          name,
          checked,
          urgent
        }
      }
    }
  }
}

Let’s try the query in the playground as before:

Now we have a database with a schema and it is fully operational with creating and fetch functionality. Similarly, we can create queries for adding, updating, removing items to a shopnote and also updating and deleting a shopnote. These queries will be used at a later point in time when we create the serverless functions.

If you are interested to run other queries in the GraphQL editor, you can find them from here,

Create a Server Secret Key

Next, we need to create a secured server key to make sure the access to the database is authenticated and authorized.

Click on the SECURITY option available in the FaunaDB interface to create the key, like so,

On successful creation of the key, you will be able to view the key’s secret. Make sure to copy and save it somewhere safe.

We do not want anyone else to know about this key. It is not even a good idea to commit it to the source code repository. To maintain this secrecy, create an empty file called .env at the root level of your project folder.

Edit the .env file and add the following line to it (paste the generated server key in the place of, <YOUR_FAUNA_KEY_SECRET>).

FAUNA_SERVER_SECRET=<YOUR_FAUNA_KEY_SECRET>

Add a .gitignore file and write the following content to it. This is to make sure we do not commit the .env file to the source code repo accidentally. We are also ignoring node_modules as a best practice.

.env

We are done with all that had to do with Fauna’s setup. Let us move to the next phase to create serverless functions and APIs to access data from the Fauna data store. At this stage, the directory structure may look like this,

Set up Netlify Serverless Functions

Netlify is a great platform to create hassle-free serverless functions. These functions can interact with databases, file-system, and in-memory objects.

Netlify functions are powered by AWS Lambda. Setting up AWS Lambdas on our own can be a fairly complex job. With Netlify, we will simply set a folder and drop our functions. Writing simple functions automatically becomes APIs. 

First, create an account with Netlify. This is free and just like the FaunaDB free tier, Netlify is also very flexible.

Now we need to install a few dependencies using either npm or yarn. Make sure you have nodejs installed. Open a command prompt at the root of the project folder. Use the following command to initialize the project with node dependencies,

npm init -y

Install the netlify-cli utility so that we can run the serverless function locally.

npm install netlify-cli -g

Now we will install two important libraries, axios and dotenv. axios will be used for making the HTTP calls and dotenv will help to load the FAUNA_SERVER_SECRET environment variable from the .env file into process.env.

yarn add axios dotenv

Or:

npm i axios dotenv

Create serverless functions

Create a folder with the name, functions at the root of the project folder. We are going to keep all serverless functions under it.

Now create a subfolder called utils under the functions folder. Create a file called query.js under the utils folder. We will need some common code to query the data store for all the serverless functions. The common code will be in the query.js file.

First we import the axios library functionality and load the .env file. Next, we export an async function that takes the query and variables. Inside the async function, we make calls using axios with the secret key. Finally, we return the response.

// query.js
 
const axios = require("axios");
require("dotenv").config();
 
module.exports = async (query, variables) => {
  const result = await axios({
      url: "https://graphql.fauna.com/graphql",
      method: "POST",
      headers: {
          Authorization: `Bearer ${process.env.FAUNA_SERVER_SECRET}`
      },
      data: {
        query,
        variables
      }
 });
 
 return result.data;
};

Create a file with the name, get-shopnotes.js under the functions folder. We will perform a query to fetch all the shop notes.

// get-shopnotes.js
 
const query = require("./utils/query");
 
const GET_SHOPNOTES = `
   query {
       allShopNotes {
       data {
         _id
         name
         description
         updatedAt
         items {
           data {
             _id,
             name,
             checked,
             urgent
         }
       }
     }
   }
 }  
`;
 
exports.handler = async () => {
  const { data, errors } = await query(GET_SHOPNOTES);
 
  if (errors) {
    return {
      statusCode: 500,
      body: JSON.stringify(errors)
    };
  }
 
  return {
    statusCode: 200,
    body: JSON.stringify({ shopnotes: data.allShopNotes.data })
  };
};

Time to test the serverless function like an API. We need to do a one time setup here. Open a command prompt at the root of the project folder and type:

netlify login

This will open a browser tab and ask you to login and authorize access to your Netlify account. Please click on the Authorize button.

Next, create a file called, netlify.toml at the root of your project folder and add this content to it,

[build]
    functions = "functions"
 
[[redirects]]
   from = "/api/*"
   to = "/.netlify/functions/:splat"
   status = 200

This is to tell Netlify about the location of the functions we have written so that it is known at the build time.

Netlify automatically provides the APIs for the functions. The URL to access the API is in this form, /.netlify/functions/get-shopnotes which may not be very user-friendly. We have written a redirect to make it like, /api/get-shopnotes.

Ok, we are done. Now in command prompt type,

netlify dev

By default the app will run on localhost:8888 to access the serverless function as an API.

Open a browser tab and try this URL, http://localhost:8888/api/get-shopnotes:

Congratulations!!! You have got your first serverless function up and running.

Let us now write the next serverless function to create a ShopNote. This is going to be simple. Create a file named, create-shopnote.js under the functions folder. We need to write a mutation by passing the required parameters. 

//create-shopnote.js
 
const query = require("./utils/query");
 
const CREATE_SHOPNOTE = `
  mutation($name: String!, $description: String!, $updatedAt: Time!, $items: ShopNoteItemsRelation!) {
    createShopNote(data: {name: $name, description: $description, updatedAt: $updatedAt, items: $items}) {
      _id
      name
      description
      updatedAt
      items {
        data {
          name,
          checked,
          urgent
        }
      }
    }
  }
`;
 
exports.handler = async event => {
  
  const { name, items } = JSON.parse(event.body);
  const { data, errors } = await query(
    CREATE_SHOPNOTE, { name, items });
 
  if (errors) {
    return {
      statusCode: 500,
      body: JSON.stringify(errors)
    };
  }
 
  return {
    statusCode: 200,
    body: JSON.stringify({ shopnote: data.createShopNote })
  };
};

Please give your attention to the parameter, ShopNotesItemRelation. As we had created a relation between the ShopNote and Item in our schema, we need to maintain that while writing the query as well.

We have de-structured the payload to get the required information from the payload. Once we got those, we just called the query method to create a ShopNote.

Alright, let’s test it out. You can use postman or any other tools of your choice to test it like an API. Here is the screenshot from postman.

Great, we can create a ShopNote with all the items we want to buy from a shopping mart. What if we want to add an item to an existing ShopNote? Let us create an API for it. With the knowledge we have so far, it is going to be really quick.

Remember, ShopNote and Item are related? So to create an item, we have to mandatorily tell which ShopNote it is going to be part of. Here is our next serverless function to add an item to an existing ShopNote.

//add-item.js
 
const query = require("./utils/query");
 
const ADD_ITEM = `
  mutation($name: String!, $urgent: Boolean!, $checked: Boolean!, $note: ItemNoteRelation!) {
    createItem(data: {name: $name, urgent: $urgent, checked: $checked, note: $note}) {
      _id
      name
      urgent
      checked
      note {
        name
      }
    }
  }
`;
 
exports.handler = async event => {
  
  const { name, urgent, checked, note} = JSON.parse(event.body);
  const { data, errors } = await query(
    ADD_ITEM, { name, urgent, checked, note });
 
  if (errors) {
    return {
      statusCode: 500,
      body: JSON.stringify(errors)
    };
  }
 
  return {
    statusCode: 200,
    body: JSON.stringify({ item: data.createItem })
  };
};

We are passing the item properties like, name, if it is urgent, the check value and the note the items should be part of. Let’s see how this API can be called using postman,

As you see, we are passing the id of the note while creating an item for it.

We won’t bother writing the rest of the API capabilities in this article,  like updating, deleting a shop note, updating, deleting items, etc. In case, you are interested, you can look into those functions from the GitHub Repository.

However, after creating the rest of the API, you should have a directory structure like this,

We have successfully created a data store with Fauna, set it up for use, created an API backed by serverless functions, using Netlify Functions, and tested those functions/routes.

Congratulations, you did it. Next, let us build some user interfaces to show the shop notes and add items to it. To do that, we will use Gatsby.js (aka, Gatsby) which is a super cool, React-based static site generator.

The following section requires you to have basic knowledge of ReactJS. If you are new to it, you can learn it from here. If you are familiar with any other user interface technologies like, Angular, Vue, etc feel free to skip the next section and build your own using the APIs explained so far.

Set up the User Interfaces using Gatsby

We can set up a Gatsby project either using the starter projects or initialize it manually. We will build things from scratch to understand it better.

Install gatsby-cli globally. 

npm install -g gatsby-cli

Install gatsby, react and react-dom

yarn add gatsby react react-dom

Edit the scripts section of the package.json file to add a script for develop.

"scripts": {
  "develop": "gatsby develop"
 }

Gatsby projects need a special configuration file called, gatsby-config.js. Please create a file named, gatsby-config.js at the root of the project folder with the following content,

module.exports = {
  // keep it empty    
}

Let’s create our first page with Gatsby. Create a folder named, src at the root of the project folder. Create a subfolder named pages under src. Create a file named, index.js under src/pages with the following content:

import React, { useEffect, useState } from 'react';    
 
export default () => {    
  const [loading, setLoading ] = useState(false);    
  const [shopnotes, setShopnotes] = useState(null);    
 
  return (
    <>    
      <h1>Shopnotes to load here...</h1>
    </>        
  )    
}

Let’s run it. We generally need to use the command gatsby develop to run the app locally. As we have to run the client side application with netlify functions, we will continue to use, netlify dev command.

netlify dev

That’s all. Try accessing the page at http://localhost:8888. You should see something like this,

Gatsby project build creates a couple of output folders which you may not want to push to the source code repository. Let us add a few entries to the .gitignore file so that we do not get unwanted noise.

Add .cache, node_modules and public to the .gitignore file. Here is the full content of the file:

.cache
public
node_modules
*.env

At this stage, your project directory structure should match with the following:

Thinking of the UI components

We will create small logical components to achieve the ShopNote user interface. The components are:

  • Header: A header component consists of the Logo, heading and the create button to create a shopnote.
  • Shopenotes: This component will contain the list of the shop note (Note component).
  • Note: This is individual notes. Each of the notes will contain one or more items.
  • Item: Each of the items. It consists of the item name and actions to add, remove, edit an item.

You can see the sections marked in the picture below:

Install a few more dependencies

We will install a few more dependencies required for the user interfaces to be functional and look better. Open a command prompt at the root of the project folder and install these dependencies,

yarn add bootstrap lodash moment react-bootstrap react-feather shortid

Lets load all the Shop Notes

We will use the Reactjs useEffect hook to make the API call and update the shopnotes state variables. Here is the code to fetch all the shop notes. 

useEffect(() => {
  axios("/api/get-shopnotes").then(result => {
    if (result.status !== 200) {
      console.error("Error loading shopnotes");
      console.error(result);
      return;
    }
    setShopnotes(result.data.shopnotes);
    setLoading(true);
  });
}, [loading]);

Finally, let us change the return section to use the shopnotes data. Here we are checking if the data is loaded. If so, render the Shopnotes component by passing the data we have received using the API.

return (
  <div className="main">
    <Header />
    {
      loading ? <Shopnotes data = { shopnotes } /> : <h1>Loading...</h1>
    }
  </div>
); 

You can find the entire index.js file code from here The index.js file creates the initial route(/) for the user interface. It uses other components like, Shopnotes, Note and Item to make the UI fully operational. We will not go to a great length to understand each of these UI components. You can create a folder called components under the src folder and copy the component files from here.

Finally, the index.css file

Now we just need a css file to make things look better. Create a file called index.css under the pages folder. Copy the content from this CSS file to the index.css file.

import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css'

That’s all. We are done. You should have the app up and running with all the shop notes created so far. We are not getting into the explanation of each of the actions on items and notes here not to make the article very lengthy. You can find all the code in the GitHub repo. At this stage, the directory structure may look like this,

A small exercise

I have not included the Create Note UI implementation in the GitHib repo. However, we have created the API already. How about you build the front end to add a shopnote? I suggest implementing a button in the header, which when clicked, creates a shopnote using the API we’ve already defined. Give it a try!

Let’s Deploy

All good so far. But there is one issue. We are running the app locally. While productive, it’s not ideal for the public to access. Let’s fix that with a few simple steps.

Make sure to commit all the code changes to the Git repository, say, shopnote. You have an account with Netlify already. Please login and click on the button, New site from Git.

Next, select the relevant Git services where your project source code is pushed. In my case, it is GitHub.

Browse the project and select it.

Provide the configuration details like the build command, publish directory as shown in the image below. Then click on the button to provide advanced configuration information. In this case, we will pass the FAUNA_SERVER_SECRET key value pair from the .env file. Please copy paste in the respective fields. Click on deploy.

You should see the build successful in a couple of minutes and the site will be live right after that.

In Summary

To summarize:

  • The Jamstack is a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup.
  • 70% – 80% of the features that once required a custom back-end can now be done either on the front end or there are APIs, services to take advantage of.
  • Fauna provides the data API for the client-serverless applications. We can use GraphQL or Fauna’s FQL to talk to the store.
  • Netlify serverless functions can be easily integrated with Fauna using the GraphQL mutations and queries. This approach may be useful when you have the need of  custom authentication built with Netlify functions and a flexible solution like Auth0.
  • Gatsby and other static site generators are great contributors to the Jamstack to give a fast end user experience.

Thank you for reading this far! Let’s connect. You can @ me on Twitter (@tapasadhikary) with comments, or feel free to follow.


The post How to create a client-serverless Jamstack app using Netlify, Gatsby and Fauna appeared first on CSS-Tricks.

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