Gutenberg 12.3 Introduces New Blocks, Design Options, and a Complete Core Blocks Reference

The first Gutenberg plugin release of the year contains loads of features. Developers should enjoy a completed core blocks reference. Group block spacing controls offer more layout flexibility, and a new Post Author Name block has arrived on the scene. Overall, the site editing experience is shaping up.

The latest release adds a couple of nice-to-have design options. The Group block now supports typography options. This should make it easier for users and theme designers to change the font size, family, and more for all child blocks at once. The Spacer block supports custom units, so users are no longer restricted to pixels. Paragraphs now have a font-family option.

Theme authors can now register “nameless” font sizes to support the core sizes. They also have new Comments Pagination Previous and Next blocks for handling paged comments.

Users without the edit_theme_options capability (Administrators by default) can no longer create nav menus via the site editor. However, they can select an existing one via the Navigation block. This returns menu creation to its original restrictions.

Let us dive right into some of the big features.

Complete Blocks Reference

While the average user may never look at it, the Core Blocks Reference in the editor handbook should be a welcome sight to developers. It is almost impossible to remember all the blocks and everything about them.

The data is automatically generated. Each block in the reference displays the following information:

  • Name
  • Category
  • Supports
  • Attributes

I would love for WordPress.org to automatically display this for all block plugins in the future. The data is standardized through block.json files, so there is no reason it should not be possible. Otherwise, individual plugin authors would need to recreate this to share their own references.

Use Site Logo as Favicon

Users can now save the image used for the Site Logo block as their site icon (favicon) via the site editor. This is a step in the right direction now that the customizer is disappearing for block theme users. They will need an easy way to set this.

Site Logo block selected in the site editor.  On the right, the Settings panel is open, showing an option for using the logo as a site icon.
New “Use as site icon” option.

There are two downsides to this solution. The first is that this option is enabled by default. If a user does not notice it, they could be overwriting their existing site icon without knowing. Logos and icons do not always match. The WP Tavern website shows a prime example of this.

The second issue is that we need a dedicated site icon option. Adding it to one of the available settings screens in the admin would be a simple solution. That would give time to figure out how to eventually work it into the site editor.

Post Author Name Block

Gutenberg 12.3 introduces a new Post Author Name block. As its name suggests, it displays a post’s author. It is something so seemingly simple, but when you have been waiting ages for it — well, since May 2021, technically — it feels monumental. To show how happy I am to see this block, I made five custom styles for it.

Several demos of the Post Author Name block shown in the WordPress editor. The first is the normal block.  The next five have emoji icons before the author name.
Tinkering with custom Post Author Name block styles.

This is a part of an effort to split the original Post Author block into smaller components. Currently, it can display the author name, avatar, bio, and custom byline text. If a theme designer needs just the author text in a one-line meta area, a user enabling those other elements via the site editor usually blows the entire thing up.

Separate Post Author Avatar and Post Author Bio blocks are planned. When these next two land, it will give theme authors more flexibility.

Group Block Spacing Control

I feel like a kid in a toyshop to see more than one of my long-time gripes taken care of in this release. The Group block now supports the Block Spacing control. This allows users and theme authors to define the margin between its child blocks.

One of the primary use cases for this is collapsing margins between similar elements. For example, a user might want to group three File blocks together without any whitespace between them, as shown in the following screenshot:

Three File block stacked without any spacing between them in the WordPress editor.
Stacked blocks.

Typically, these blocks would have the standard top margins between each. By setting the block spacing to 0, users can wipe them out.

“Zeroing out” margins has been one of the most frustrating problems to solve for some themers, especially when dealing with wide or full-aligned blocks. The WordPress editor’s HTML markup does not match the front-end in those cases. A block spacing control on the Group block solves this issue for several scenarios.

Of course, it is not all about getting rid of margins between blocks. It is also possible to create even more space.

When using the Row variation of the Group block, the spacing is horizontal. This should also take care of a lot of theme layout needs.

Notes on Josh Comeau’s Custom CSS Reset

We recently talked with Elad Shechter on his new CSS reset, and shortly after that Josh Comeau blogged his.

We’re in something of a new era of CSS resets where… you kind of don’t need one? There isn’t that many major differences between browsers on default styling, and by the time you’re off and running styling stuff, you’ve probably steamrolled things into place. And so perhaps “modern” CSS resets are more of a collection of opinionated default styles that do useful things that you want on all your new projects because, well, that’s how you roll.

Looking through Josh’s choices, that’s what it seems like to me: a collection of things that aren’t particularly opinionated about design, but assist the design by being things that pretty much any project will want.

I’m gonna go through it and toss out 🔥 flamin’ hot opinions.

*, *::before, *::after {
  box-sizing: border-box;
}

Heck yes. We used to consider this a global holiday ’round here. Although, with more and more layout being handled by grid and flexbox, I’m feeling like this is slightly less useful these days. When you’re setting up a layout with fr units and flexin’ stuff, the box-sizing model doesn’t come into play all that much, even when padding and border are involved. But hey, I still prefer it to be in place. I do think if it goes into a CSS reset it should use the inheritance model though, as it’s easier to undo on a component that way.

* {
  margin: 0;
}

This is basically why the CSS-Tricks logo “star” exists. I used to love this little snippet in my CSS resets. There was a period where it started to feel heavy-handed, but I think I’m back to liking it. I like how explicit you have to be when applying any margin at all. Personally, I’d rock padding: 0; too, as list elements tend to have some padding pushing them around. If you’re nuking spacing stuff, may as well nuke it all.

html, body {
  height: 100%;
}

Probably a good plan. Josh says “Allow percentage-based heights in the application,” which I can’t say comes up much in my day-today, but what it does is stuff like the body background not filling the space the way you might expect it to.

Too bad body { height: 100vh; } isn’t enough here, but I feel like that’s not as reliable for some reason I can’t think of right now. Maybe something to do with the footer navigation in iOS Safari?

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

I can’t get into the -webkit-font-smoothing: antialiased; thing. I think it tends to make type dramatically thin and I don’t love it. I don’t mind it as a tool, but I wouldn’t globally apply it on all my projects.

I also generally like to put global typographic sizing stuff on the html selector instead, just because the “root” part of rem implies the <html> element — not the <body> — and I like sizing stuff in rem and then adjusting the root font-size at the root level in media queries.

That 1.5 value feels like a good default line-height (more of a 1.4 guy myself, but I’d rather go up than down). But as soon as it’s set, I feel magnetically pulled into reducing it for heading elements where it’s always too much. That could happen via h1, h2, h3 kinda selectors (maybe h4h6 don’t need it), but Josh has some CSS trickery at work with this snippet that didn’t make it into the final reset:

* {
  line-height: calc(1em + 0.5rem);
}

That’s clever in how the 0.5rem goes a long way for small type, but isn’t as big of an influence for large type. I could see trying that on a greenfield project. Prior art here is by Jesús Ricarte in “Using calc to figure out optimal line-height.”

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

Good move for a CSS reset. The block display type there prevents those annoying line-height gaps that always kill me. And you almost never want any of these media blocks to be wider than the parent. I somehow don’t think picture is necessary, though, as it’s not really a style-able block? Could be wrong. I’d probably toss iframe and object in there as well.

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

Good move for sure. It’s bad news when a long word (like a URL) forces an element wide and borks a layout. I tend to chuck this on something — like article or .text-content or something — and let it cascade into that whole area (which would also catch text that happens to be contained in an improper element), but I don’t mind seeing it on specific text elements.

If doing that, you probably wanna chuck li, dl, dt, blockquote on that chain. Despite having attempted to research this several times (here’s a playground), I still don’t 100% know what the right cocktail of line-wrapping properties is best to use. There is word-break: break-word; that I think is basically the same thing. And I think it’s generally best to use hyphens: auto; too… right??

#root, #__next {
  isolation: isolate;
}

I don’t quite understand what’s happening here. I get that this is a React/Next thing where you mount the app to these roots, and I get that it makes a stacking context, I just don’t get why it’s specifically useful to have that stacking context at this level. At the same time, I also don’t see any particular problem with it.

All in all — pretty cool! I always enjoy seeing what other people use (and go so far as to suggest) for CSS resets.


Notes on Josh Comeau’s Custom CSS Reset originally published on CSS-Tricks. You should get the newsletter and become a supporter.

PHP – Incrementing element with the same name.

If you download multiple files with the same name, a number is automatically added to the file name, for example: filename(1), filename(2) etc..

What I am trying to do is when I am creating a name and there is already a same one, the name I create to appear in the list as: name(1), name(2) etc.

This is the code that I use now that only creates: name and name (1), then I get this error when trying to create the next same name, example: name (1) already exists in your list., but I shouldn't get that error and should create the next name in the list: name (2) etc..

$deviceUser = "07NAV" . $var;
$sameNames = $mktApi->comm("/ppp/secret/getall", array(
    ".proplist" => ".id",
    "?name" => $deviceUser
));

$i = 0;
if($sameNames) {
    $i++;
    $mktApi->comm("/ppp/secret/add", array(
        "name" => $deviceUser . " ($i)",
        "remote-address" => $IPs[$ipAddress],
        "password" => $devicePass,
        "service" => "pppoe",
        "comment" => $fullAddress
    ));
}

Also, I've tried using this one:

$i = 0;
if($sameNames) {
    for($i = 0; $i < 99999; $i++) {
        $i++;
        $mktApi->comm("/ppp/secret/add", array(
            "name" => $deviceUser . " ($i)",
            "remote-address" => $IPs[$ipAddress],
            "password" => $devicePass,
            "service" => "pppoe",
            "comment" => $fullAddress
        ));
    }
}

but when I create only one name in my list, automatically creates X names and I do not want that.
I've been searching for help on other communities too and somebody provided me this code, but I still get the same error ("user with same name already exists"):

// Start your update here:
$i = 0;
$deviceUser = $deviceUserOriginal = "07NAV" . $var;
$sameNames = false;
do {
    // First - check if a dupe exists...
    $sameNames = $mktApi -> comm("/ppp/secret/getall", array(
        ".proplist" => ".id",
        "?name" => $deviceUser
    ));

    // Second - update and prepare for rechecking ...
    if($sameNames === true) {
        $i ++;
        $deviceUser = $deviceUserOriginal . "(". $i .")";
    }

    // Finally, below, if check failed, cycle and check again with the new updated name...
}
while($sameNames === true);

// Finally, tidy up.
// If you need the original value of $deviceUser you can retain it.
unset($i, $deviceUserOriginal);

// Continue your script here:
$mktApi -> comm("/ppp/secret/add", array(
    "name" => $deviceUser,
    "remote-address" => $IPs[$ipAddress],
    "password" => $devicePass,
    "service" => "pppoe",
    "comment" => $fullAddress
));

C program random walk

Dog leaves his lamppost on summer evenings and staggers randomly either two steps in the direction toward home or one step in the opposite direction. After taking these steps, the dog again staggers randomly two steps toward home or one step backward and does this again and again. If the pet reaches a total distance of 10 steps from the lamppost in the direction toward home, you find him and take him home. If the dog arrives back at the lamppost before reaching 10 steps in the direction toward home, he lies down and spend the night at the foot of the lamppost. Write a C program that stimulates 500 summer evenings, and calculate and print the percentage of the time your pet sleeps at home for these evenings. Accumulate the distance the dog has reached toward your home. If the distance reaches 10, stop the loop and increment the home count. If the distance reaches 0 before it reaches 10, stop the loop but do not increment the home count. Repeat this loop 500 times and find the ratio of (home count)/500.0.

FOR a dummy-How to connect html webpage with SQL table data

is having created a webpage using Html and css the page is laidout (has header, side panel, main content area and a footer) as well as (a table of appx 2 by columns and rows); I will create a very small SQL table with data; BUT how do I transfer/export the sql data onto any place of my webpage?; or from the webpage into the SQL table?

Improving Performance With SQL Aggregate Functions

In this article, you will learn how SQL aggregate functions can represent an easy way to significantly improve your application's performance. Mainly, you will see how they were a game-changer in a real-world scenario based on a data-driven application developed for a startup operating in the sports industry.

Let's now delve deeper into this scenario and learn why you can't ignore SQL aggregate functions in data science.

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>([\s\S]*?)<\/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>([\s\S]*?)<\/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.

Web Application Security Driven by WAF is Favoring Organizations

Web application security through WAF is a great way to protect a company's web applications from hacking attempts. It provides comprehensive security for the server's web applications, allowing the company to protect the data stored in back-end databases. While a WAF cannot provide the same level of protection as a dedicated firewall, it can prevent data leakage in a secure manner.

First-Generation WAF

Although first-generation WAFs scanned web traffic without detecting malicious content, these models did not offer the highest level of security. They weren't flexible enough to handle constant software updates, and a stateless WAF could not prevent attackers from devising new attack behaviors. Additionally, they were too expensive, offered false positives, and required dedicated IT expertise to maintain.

349: With Olivia

Olivia Ng has done loads of wonderful work here on CodePen and off (check out her super cool travel bucket list site) She got started just out of pure desire to build things. “I just really like the internet” she told me. Hear hear! Her eye for design takes all her work to the next level. She had a particular focus on grid for a while there, and used those interesting designs to teach it. Also find her on Twitter and on her personal website.

Time Jumps

  • 00:25 Guest introduction
  • 02:00 How did you get into coding?
  • 05:45 Design as a way into code
  • 07:01 Sponsor: Jetpack
  • 08:59 Blogging interesting designs
  • 12:07 Deciding on what features to use
  • 14:58 Designing toggles
  • 16:19 Makeup color grid
  • 20:03 Working in one sitting vs over a longer period of time
  • 22:25 What was your employment story?
  • 26:56 Working in 3D on CSS
  • 29:05 Is work fun enough or do side projects help to be creative?
  • 31:02 Rotalics

Sponsor: Jetpack

There are lots of reasons to look at Jetpack for your self-hosted WordPress site. One of which is the powerful search upgrade you get just by flipping a switch. Say you run a lot of WordPress sites, perhaps for clients as an agency would, now Jetpack offers a Licesning Dashboard for managing all the Jetpack subscriptions so that becomes a lot easier.

The post 349: With Olivia appeared first on CodePen Blog.

Batch Processing in Go

Batching is a common scenario developers come across to basically split a large amount of work into smaller chunks for optimal processing. Seems pretty simple, and it really is. Say we have a long list of items we want to process in some way. A pre-defined number of them can be processed concurrently. I can see two different ways to do it in Go.

The first way is by using plain old slices. This is something most developers have probably done at some point in their careers. Let's take this simple example:

Is Spring Boot Still State of the Art?

In the following blog post, I want to take a closer look at the question of whether the application framework Spring Boot is still relevant in modern Java-based application development. I will take a critical look at its architectural concept and compare it against the Jakarta EE framework. I am aware of how provocative the question is and that it also attracts incomprehension. Comparing both frameworks I am less concerned about the development concept but more with the question about runtime environments. 

Spring Boot and Jakarta EE Logos

Both – Spring Boot and Jakarta EE – are strong and well-designed concepts for developing modern Microservices. When I am talking about Jakarta EE and Microservices I always talk also about Eclipse Microprofile which is today the de-facto standard extension for Jakarta EE. Developing a Microservice the concepts of Spring Boot and Jakarta EE are both very similar. The reason is, that a lot of technology of today’s Jakarta EE was inspired by Spring and Spring Boot. The concepts of “Convention over Configuration“, CDI, or the intensive usage of annotations were first invited by Spring. And this is proof of the innovative power of Spring and Spring Boot. But I believe that Jakarta EE is today the better choice when looking for a Microservice framework. Why do I come to this conclusion?

Ten Tips For Aspiring Designer Beginners (Part 1)

I’ve been living and breathing user interface and product design for just over a decade now, and although I’ve had the pleasure of working alongside some incredibly helpful and creative colleagues, my formal guidance (or teaching, as I should probably say) has been rather limited.

In this article, I want to share ten tips that helped me grow and become a better designer, and I hope these tips will also help you while you’re trying to find more solid ground under your feet. In this first part of the article, we’ll start with the first five tips, and the rest will continue into the second part.

This article is aimed at all design beginners out there — willing to learn UI (user interface), UX (user experience), and product design. But before I start sharing my advice here, it would probably be a good idea to first explain who I am and where I’m coming from. My tips have to be grounded in at least some sort of success, right?

I’ve been working as a designer for just over ten years now, which is both exciting and horrifying. I’ve been in almost every industry, spanning e-commerce, charity, hospitality, travel, and startup, mostly as a solo designer. In fact, the majority of my career has been spent shooting from the hip, in the darkness, as I tried to drive design operations forward in a mostly non-design environment — fun! This exposure forced me to stretch my skillset outside of just being a designer concerned about aligning pixels perfectly, to one interested in the broader question of, “Will this sell more products?” Today, I’ll explain why I believe you should try to do that as well.

I now find myself working as a Designer Advocate at Figma. That’s right; I’ve parked day-to-day design work in favor of becoming someone very active in the design community, focusing on best practice design advice and scalable systems.

1. We’re All Faking It

No one really knows what they are doing. I know that “fake it ‘till you make it” is not a new idea which is why I will not be advising you to do that. Well, kind of.

“Fake it till you become it. Do it enough until you actually become it and internalize.”

Amy Cuddy, Professor at Harvard Business School

In the design and tech industry, the first thing we all need to realize is that the vast majority of people really do not know how to get through the day. The best way to become a millionaire in tech is to be granted a penny for every time you will work in a team that has no idea what they are doing but are pushing forward anyway.

It’s not their fault though — it’s the system’s! Our industry has a low barrier to entry, and a lot of venture capital money is being pumped into it, meaning that we’re effectively in a gold rush. With entrance into the industry being so easy and lack of proper benchmarking (Note: this is somewhat contradictory to point 2, but more on that later) around what makes a good designer, software engineer, or product manager, we’re forced to face the facts that it’s a recipe for poor quality products.

Alright, enough doom and gloom. How can you navigate these waters? The first step is admittance. You will persistently work with people who don’t meet your high standards, and you have an opportunity to leapfrog this incompetence if you play your cards right. It’s only at this stage that you can propel yourself fast into a successful career, and not be “left behind” building products you actually don’t want to put in your portfolio.

Remember earlier when I mentioned that it’s important to become a “rounded” employee? Ding-dong, now is your time. Building strong interpersonal skills, becoming someone who is willing to make a decision and run with it, and understanding that if you don’t, someone else will, is where you will make progress.

The very cold truth is that even designers with ten years of experience are asking seemingly silly questions, such as “What is the pixel width of an iPhone 12?” (Hint, that was me this week.) Sure, that’s a seemingly foolish question to ask, but the important thing is that you actually do ask for help (and that’s because you do not and simply cannot know everything).

A previous manager of mine accused me of “spinning my wheels” at the start of every project, which took me a while to understand, but hey, he was absolutely right. How was I trying to work everything out myself going to help the team, and ultimately the business? You guessed it, it wasn’t. Admitting this to myself was a huge step in becoming a better designer, colleague, and professional.

2. Finding A Job Is Really… Really Hard

Hiring is broken. Allow me to share my advice on navigating the oceans of employment.

I was once stuck in a full-time job.

Stuck? Yes, stuck.

I wasn’t progressing, our projects were being rejected or de-prioritized by management, and I spent months designing a mobile website and a separate desktop experience for two products only for these to be swiftly brushed to the side. I had actually been applying for jobs for... 12 months.

You read that right. I was trying to find a new job for an entire year without a single first-stage interview.

I was fortunate enough to have a job in the first place, otherwise things could’ve become very tricky, very fast. The fact is that my portfolio wasn’t shiny enough, I had no real presence in the industry, and no connections to rely on for warm introductions to other companies.

This is what happens when we focus too much on what’s going on inside our boxes, rather than taking a look outside for a moment and do our best to understand the reasons why we’re personally not progressing. At this job, I was the go-to person for all design-related queries, I was driving new money-making initiatives forward (I introduced a newsletter scheme that brought in an extra £10k per month and helped to scale our subscriber list from 60k to 500k in around 9 months), but it was seemingly insignificant on my CV — and soul-destroying, as you can imagine.

So what did I do? I left the country. Actually, the continent. In fact, I moved as far away as was possible — from London to sunny Sydney.

Mmm, now that was quite a drastic move, but you know what? It was required. I needed to reinvent myself outside of the shackles of my existing position. I was working in a place that was failing to help me package up my skills in a way that the London job market required. By the way, I found a new job before I’d even arrived in Sydney. (It’s amazing what a bit of direction can do for your motivation.) In fact, I found an even better job within the first three weeks of this new job I was working at — I became a product designer at a hospitality startup.

You see, the problem with my situation being stuck in London was that I wasn’t looking beyond my computer to appreciate what skills I had acquired over the years and how that could shape a new career projection.

Now, let me stop talking about me for a second, and ask you what you can take away from this experience? Firstly, you are employable. You just need to shape your narrative. Are you interested in the agile design process? Okay, show us how and why. Are you also pretty good at illustration? Show it off. Did you take a short course in front-end development basics? Make it known.

At the risk of sounding a bit like a broken record, it comes right back to being as round as you can as a candidate. We can all design a login screen in Figma (or in Illustrator, Affinity Designer, or any design app of your choice), but can all of us talk about it, write the copy, apply some illustrative flourish, or plan the entire user journey? No.

Knuckle down on where your interests lie on this scale and make sure you shout about it in your job applications because it’s this difference that will make you stand out.

The preaching section of this point is over, let’s get back to some more practical guidance.

So far, in my career (seven full-time jobs), I’ve found positions via:

  • An obscure internship website that I wouldn’t be able to remember or find ever again even if I tried;
  • A friend’s referral (the job was poorly advertised);
  • LinkedIn job search;
  • Facebook freelancers group;
  • A Slack channel for Australian designers;
  • Twitter search and direct messages follow-up.

Are you seeing anything interesting here? Well, you should notice that there is only one job listed that I found via a traditional platform — and guess what, this was the job I was stuck at.

My career has been a series of lucky strikes (although I do believe you make your own luck, however, this is a topic for another article) where I’ve gone into places to find positions that are not traditional, discovering jobs that are either poorly applied for, or poorly advertised; or both, to be honest. Why is this important? The more obscure the application, the fewer candidates will apply, and the easier it will be for you to shine.

Note: This does not apply for people who qualify under point 8 (coming up in Part 2) because your breaks are already lucky.

If you’re struggling to get interviews, the likelihood is that you need to follow a similar path that I did and become a bit more of a detective, finding the companies that struggle to spend large amounts of money on advertising positions. Get yourself into designer Slack groups; dig around Twitter (I honestly searched “product designer London” for one of my positions and direct messaged the CEO who then invited me in for an interview the next week based on my (now old) Behance work), or scour LinkedIn hashtags rather than blindly applying to every company you recognize the logo of.

Starting out your career is tough, and companies need that extra level of comfort when bringing juniors in, so let’s make their lives a bit easier by spelling out our skills and applying them in the places others don’t think of.

3. We’re All Worthy!

Always keep in mind to communicate your value. Following on from the previous point, proving that you know your stuff is really tough. Trust me, I’m the guy that had zero interviews in twelve months, remember?

So how can we package up our skills effectively? My past few jobs have become smoother application processes because I’ve landed on a formula that makes your life and the recruiter’s life easier, and that formula is being concise. Of course, this is coupled with my previous advice on finding jobs that no one else has.

This could be the point where I paste in a list of “here’s how to write an effective CV” articles, but firstly I’m not that lazy, and secondly, I don’t believe in most of what they say because the advice shared in these articles ignores most of what you’ve just been reading about luck.

In saying that, there is a way we can package ourselves up to make sure that when the recruiter spends their customary 30 seconds scanning through our application, we can be remembered.

And it’s incredibly simple: use bullet points. The second way to become a millionaire in tech is to receive a penny for every time you read something where one hundred words are used instead of ten. Be concise! Time is precious, and when someone is scanning (yes, scanning, not reading) two hundred applications for a design job, yours will simply not make the cut if you can’t explain quickly your value. Speaking of which, here’s your CV starter kit:

  • Note your team or business wins rather than pixel-pushing projects.
    • Did the company recently go through funding? Were you involved?
    • Did something significant just happen on a project you worked on? Maybe a boost in subscriptions or signups? Talk about it.
  • Have you led anything?
    • What specifically did you lead and can you attempt to describe the impact?
  • What cross-functional impact have you had?
    • Maybe you worked in partnership with the engineering lead on X to increase the efficiency of Y — I’d love to hear more about this during an interview.

And portfolio starter:

  • Who worked on the project;
  • What you built;
  • Why you built it;
  • When it was built;
  • Where it was marketed/showcased;
  • How you had an impact.

Think in actions here, rather than describing what you’ve done. For example, having a bullet point of “increased conversions on our signup form by 10%” on your application is something I can immediately be impressed with, and will have the hiring manager nodding in approval (rather than statements such as “worked on the website” which doesn’t tell anyone anything about what you actually did) — and can help get you into that interview with confidence.

I mentioned this before, but it’s worth hammering home. If you have skills different but complementary to the specific position you’re applying for (e.g. you are a designer who enjoys writing), then add these as a footnote to your CV. I also like to include an “ongoing side projects” section in my CV to show that I'm interested in other things.

Note: It’s not a requirement to work on side projects. Being able to “work” outside of work is a privilege I fully appreciate, and the industry should not hold it against those who just want to work their contracted hours and get on with their lives in their free time.

4. Prioritize Networking Over Pushing Pixels

Community is everything in the industry. It took me seven years to realize that the more I invested in the industry, the more I’d get back. Sure, I had a Twitter account with a few hundred followers that I’d occasionally chat with, but this was second to trying to learn Photoshop (yes, I designed websites in this app) or Sketch (also in this app) inside out.

The fact is that no one tells us that we should be working harder on making good connections rather than polishing our designs or code. Why would they? It’s the industry’s best-kept secret. Heck, I wouldn’t have landed my Figma job as a community ambassador without some investment in the community itself!

What does this actually mean then? Dialing back to my big Sydney reinvention move, I decided (you know, because I didn’t know anyone) to get involved in local design events. I immediately fell in love with this access to other people trying to work out similar problems in a safe space.

I would frequently attend a small design critique monthly event (otherwise known simply as a “crit” among designers with more years of experience), which I ultimately ended up running a few of, and also attended a monthly speaker series which encouraged people to share tips if they wanted to. Being someone who loves the microphone, I shared a few tips too at a couple of these events. Then the next thing I knew, I was being asked to co-host — and I jumped at the opportunity.

At the same time, I fell back in love with writing. These two non-designer skills were paramount in becoming a better all-rounder — and here’s that phrase again. Being able to communicate — whether that’s holding a microphone at an event, describing your decision-making to colleagues, or documenting your work in an article — is an incredibly important skill as you move up the career ladder. The best designers (and managers, to be honest) are the ones who communicate often, in detail, and to the benefit of the team. So, start getting into the habit of documenting your work now, and it’ll push you forward a lot faster in the long run.

Tip: Hover around at industry events, maybe dabble in replying to some social media posts of designers you know (or want to know!), pop into some design Slack or Discord communities, and just become well… known. I realize it’s frightening, but as I explained earlier, we do all know something, so grab a hold of that and contribute to the community.

Most big companies prioritize internal referrals to blind applications, so take this and run with it. Become friends with people at companies you want to work at, or just become friends because we’re all human and need real connections with other people.

5. Learn By Doing, Not By Asking

Inspiration is (almost) worthless. We’ve all been there, doom-scrolling through design inspiration websites waiting for the spark to hit, and then ultimately just copying something cool we like the look of even though it doesn’t fit the style of the product we’re working on.

This is not how good design is produced.

At the same time, it’s tempting to ask others for the elixir to achieve perfect designs. However, the reality is simple — practice makes perfect. Or, as Pablo Picasso once said,

“Inspiration exists, but it has to find you working.”

There’s a lot of truth in this saying.

Having the unfortunate career exposure to being the only designer in the team meant that I didn’t receive the mentorship or direction from a senior designer and just had to… do it (please don’t sue me, Nike!).

I’ve always been the kind of person to try and learn just enough to impress and I’m also a stickler for shortcuts. I was the go-to guy at the university for helping with Adobe Flash, because I spent my evenings bumping into every possible error message that could appear while I worked with the Flash program, and then I’ve learned all the solutions.

So, rather than focusing on asking someone else how to do something, let’s focus on finding the answers ourselves. Google search is really your best friend; and if it doesn’t help, then there are also these designer community channels which I mentioned earlier and which will come in handy when finding the solution to something very fast.

This approach really pays off as you build up more and more expertise in the field. Being self-taught isn’t a problem, especially in a world where you’re one click away from finding out almost everything you need to know about the industry because all you need to do is search and then try to apply the bits of knowledge that you’ve just found.

When it comes to the process of design, the only way we can build user-friendly and scalable interfaces is again by doing, rather than only by reading countless articles about what we might want to do. Open up your favorite design application and start playing. Move elements around, zoom in and out, and keep experimenting.

Important: Nothing can replace a solid foundation in your work. By creating (or relying on, if it’s been defined elsewhere) strong and accessible color palettes, a good typography hierarchy, and an underlying spacing rhythm, your designs will start to “feel” right faster than it takes for you to design a meme and tweet it out.

A few recommended readings (a.k.a. setting up the foundation) before we proceed further

Conclusion

OK, hopefully by now I’ve helped you set the most important foundations when starting out your design career. I’ll continue with my next five tips in Part 2 that is already scheduled for next week. Stay tuned!

In the meantime, if you have comments or want to share some feedback and ask questions, then please do so by using the comment form below or drop me a message on Twitter. I’d be happy to help!

What Does 2022 Have in Store for Cybersecurity and Cloud Security Specialists?

Cloud adoption and industry transformation are accelerating as the world looks for efficiency. Let’s face it, 2022 promises to be another busy year for cybersecurity and cloud security specialists. 

According to the 2021 ISC Cybersecurity Workforce Study, we are still short 2.7 million cybersecurity professionals globally. There aren’t enough people to keep up with the rising threat, so we need to deploy automation heavily to tackle it. 

2022 New Year’s Resolution: Dev and SEC Cross-Team Learning

A good DevSecOps strategy goes beyond having the right tools and processes in place: it requires consistent and crucially, bi-directional feedback and learning. Both security and engineering teams have such different priorities and strengths, but that doesn’t mean they don’t have anything to learn from each other. This year, make it a resolution to create a culture of bi-directional learning between these two teams and reap the benefit of improved collaboration. Here are the top things one can learn from the other to break down silos in the name of DevSecOps.

What Security Teams Can Learn From Developers

Part of embracing DevSecOps requires relinquishing some amount of control. Security isn’t going anywhere anytime soon, but to fully embrace DevSecOps, they have to relinquish some amount of control. They have to rely on training and automation and trust that developers are capable of securing their own code.

Crafting Scroll Based Animations in Three.js

Having an experience composed of only WebGL is great, but sometimes, you’ll want the experience to be part of a classic website.

The experience can be in the background to add some beauty to the page, but then, you’ll want that experience to integrate properly with the HTML content.

In this tutorial, we will:

  • learn how to use Three.js as a background of a classic HTML page
  • make the camera translate to follow the scroll
  • discover some tricks to make the scrolling more immersive
  • add a cool parallax effect based on the cursor position
  • trigger some animations when arriving at the corresponding sections
See the live version

This tutorial is part of the 39 lessons available in the Three.js Journey course.

Three.js Journey is the ultimate course to learn WebGL with Three.js. Once you’ve subscribed, you get access to 45 hours of videos also available as text version. First, you’ll start with the basics like the reasons to use Three.js and how to setup a simple scene. Then, you’ll start animating it, creating cool environments, interacting with it, creating your own models in Blender. To finish, you will learn advanced techniques like physics, shaders, realistic renders, code structuring, baking, etc.

As a member of the Three.js Journey community, you will also get access to a members-only Discord server.

Use the code CODROPS1 for a 20% discount.

Starter

This tutorial is intended for beginners but with some basic knowledge of Three.js.

Installation

For this tutorial, a starter.zip file is provided.

You should see a red cube at the center with “My Portfolio” written on it:

The libraries are loaded as plain <script> to keep things simple and accessible for everyone:

  • Three.js in version 0.136.0
  • GSAP in version 3.9.1

For specific techniques like Three.js controls or texture loading, you are going to need a development server, but we are not going to use those here.

Setup

We already have a basic Three.js setup.

Here’s a quick explaination of what each part of the setup does, but if you want to learn more, everything is explained in the Three.js Journey course:

index.html

<canvas class="webgl"></canvas>

Creates a <canvas> in which we are going to draw the WebGL renders.

<section class="section">
    <h1>My Portfolio</h1>
</section>
<section class="section">
    <h2>My projects</h2>
</section>
<section class="section">
    <h2>Contact me</h2>
</section>

Creates some sections with a simple title in them. You can add whatever you want in these.

<script src="./three.min.js"></script>
<script src="./gsap.min.js"></script>
<script src="./script.js"></script>

Loads the Three.js library, the GSAP library, and to finish, our JavaScript file.

style.css

*
{
    margin: 0;
    padding: 0;
}

Resets any margin or padding.

.webgl
{
    position: fixed;
    top: 0;
    left: 0;
}

Makes the WebGL <canvas> fit the viewport and stay fixed while scrolling.

.section
{
    display: flex;
    align-items: center;
    height: 100vh;
    position: relative;
    font-family: 'Cabin', sans-serif;
    color: #ffeded;
    text-transform: uppercase;
    font-size: 7vmin;
    padding-left: 10%;
    padding-right: 10%;
}

section:nth-child(odd)
{
    justify-content: flex-end;
}

Centers the sections. Also centers the text vertically and aligns it on the right for one out of two sections.

script.js

/**
 * Base
 */
// Canvas
const canvas = document.querySelector('canvas.webgl')

// Scene
const scene = new THREE.Scene()

Retrieves the canvas from the HTML and create a Three.js Scene.

/**
 * Test cube
 */
const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial({ color: '#ff0000' })
)
scene.add(cube)

Creates the red cube that we can see at the center. We are going to remove it shortly.

/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

Saves the size of the viewport in a sizes variable, updates that variable when a resize event occurs and updates the camera and renderer at the same time (more about these two right after).

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 100)
camera.position.z = 6
scene.add(camera)

Creates a PerspectiveCamera and moves it backward on the positive z axis.

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

Creates the WebGLRenderer that will render the scene seen from the camera and updates its size and pixel ratio with a maximum of 2 to prevent performance issues.

/**
 * Animate
 */
const clock = new THREE.Clock()

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

Starts a loop with a classic requestAnimationFrame to call the tick function on each frame and animates our experience. In that tick function, we do a render of the scene from the camera on each frame.

The Clock lets us retrieve the elapsed time that we save in the elapsedTime variable for later use.

HTML Scroll

Fix the elastic scroll

In some environments, you might notice that, if you scroll too far, you get a kind of elastic animation when the page goes beyond the limit:

While this is a cool feature, by default, the back of the page is white and doesn’t match our experience.

We want to keep that elastic effect for those who have it, but make the white parts the same color as the renderer.

We could have set the background-color of the page to the same color as the clearColor of the renderer. But instead, we are going to make the clearColor transparent and only set the background-color on the page so that the background color is set at one place only.

To do that, in /script.js, you need to set the alpha property to true on the WebGLRenderer:

const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    alpha: true
})

By default, the clear alpha value is 0 which is why we didn’t have to set it ourselves. Telling the renderer to handle alpha is enough. But if you want to change that value, you can do it with setClearAlpha:

renderer.setClearAlpha(0)

We can now see the back of the page which is white:

In /style.css, add a background-color to the html in CSS:

html
{
    background: #1e1a20;
}

We get a nice uniform background color and the elastic scroll isn’t an issue anymore:

Objects

We are going to create an object for each section to illustrate each of them.

To keep things simple, we will use Three.js primitives, but you can create whatever you want or even import custom models into the scene.

In /script.js, remove the code for the cube. In its place, create three Meshes using a TorusGeometry, a ConeGeometry and a TorusKnotGeometry:

/**
 * Objects
 */
// Meshes
const mesh1 = new THREE.Mesh(
    new THREE.TorusGeometry(1, 0.4, 16, 60),
    new THREE.MeshBasicMaterial({ color: '#ff0000' })
)
const mesh2 = new THREE.Mesh(
    new THREE.ConeGeometry(1, 2, 32),
    new THREE.MeshBasicMaterial({ color: '#ff0000' })
)
const mesh3 = new THREE.Mesh(
    new THREE.TorusKnotGeometry(0.8, 0.35, 100, 16),
    new THREE.MeshBasicMaterial({ color: '#ff0000' })
)

scene.add(mesh1, mesh2, mesh3)

All the objects should be on top of each other (we will fix that later):

In order to keep things simple, our code will be a bit redundant. But don’t hesitate to use arrays or other code structuring solutions if you have more sections.

Material

Base material

We are going to use the MeshToonMaterial for the objects and are going to create one instance of the material and use it for all three Meshes.

When creating the MeshToonMaterial, use '#ffeded' for the color property and apply it to all 3 Meshes:

// Material
const material = new THREE.MeshToonMaterial({ color: '#ffeded' })

// Meshes
const mesh1 = new THREE.Mesh(
    new THREE.TorusGeometry(1, 0.4, 16, 60),
    material
)
const mesh2 = new THREE.Mesh(
    new THREE.ConeGeometry(1, 2, 32),
    material
)
const mesh3 = new THREE.Mesh(
    new THREE.TorusKnotGeometry(0.8, 0.35, 100, 16),
    material
)

scene.add(mesh1, mesh2, mesh3)

Unfortunately, it seems that the objects are now black:

The reason is that the MeshToonMaterial is one of the Three.js materials that appears only when there is light.

Light

Add one DirectionalLight to the scene:

/**
 * Lights
 */
const directionalLight = new THREE.DirectionalLight('#ffffff', 1)
directionalLight.position.set(1, 1, 0)
scene.add(directionalLight)

You should now see your objects:

Position

By default, in Three.js, the field of view is vertical. This means that if you put one object on the top part of the render and one object on the bottom part of the render and then you resize the window, you’ll notice that the objects stay put at the top and at the bottom.

To illustrate this, temporarily add this code:

mesh1.position.y = 2
mesh1.scale.set(0.5, 0.5, 0.5)

mesh2.visible = false

mesh3.position.y = - 2
mesh3.scale.set(0.5, 0.5, 0.5)

The torus stays at the top and the torus knot stays at the bottom:

When you’re done, remove the code above.

This is good because it means that we only need to make sure that each object is far enough away from the other on the y axis, so that we don’t see them together.

Create an objectsDistance variable and choose a random value like 2:

const objectsDistance = 2

Use that variable to position the meshes on the y axis. The values must be negative so that the objects go down:

mesh1.position.y = - objectsDistance * 0
mesh2.position.y = - objectsDistance * 1
mesh3.position.y = - objectsDistance * 2

Increase the objectsDistance until the objects are far enough apart. A good amount should be 4, but you can go back to change that value later.

const objectsDistance = 4

Now, we can only see the first object:

The two others will be below. We will position them horizontally once we move the camera with the scroll and they appear again.

The objectsDistance will get handy a bit later, which is why we saved the value in a variable.

Permanent rotation

To give more life to the experience, we are going to add a permanent rotation to the objects.

First, add the objects to a sectionMeshes array:

const sectionMeshes = [ mesh1, mesh2, mesh3 ]

Then, in the tick function, loop through the sectionMeshes array and apply a slow rotation by using the elapsedTime already available:

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Animate meshes
    for(const mesh of sectionMeshes)
    {
        mesh.rotation.x = elapsedTime * 0.1
        mesh.rotation.y = elapsedTime * 0.12
    }

    // ...
}

All the meshes (though we can see only one here) should slowly rotate:

Camera

Scroll

It’s time to make the camera move with the scroll.

First, we need to retrieve the scroll value. This can be done with the window.scrollY property.

Create a scrollY variable and assign it window.scrollY:

/**
 * Scroll
 */
let scrollY = window.scrollY

But then, we need to update that value when the user scrolls. To do that, listen to the 'scroll' event on window:

window.addEventListener('scroll', () =>
{
    scrollY = window.scrollY

    console.log(scrollY)
})

You should see the scroll value in the logs. Remove the console.log.

In the tick function, use scrollY to make the camera move (before doing the render):

const tick = () =>
{
    // ...

    // Animate camera
    camera.position.y = scrollY

    // ...
}

Not quite right yet:

The camera is way too sensitive and going in the wrong direction. We need to work a little on that value.

scrollY is positive when scrolling down, but the camera should go down on the y axis. Let’s invert the value:

camera.position.y = - scrollY

Better, but still too sensitive:

scrollY contains the amount of pixels that have been scrolled. If we scroll 1000 pixels (which is not that much), the camera will go down of 1000 units in the scene (which is a lot).

Each section has exactly the same size as the viewport. This means that when we scroll the distance of one viewport height, the camera should reach the next object.

To do that, we need to divide scrollY by the height of the viewport which is sizes.height:

camera.position.y = - scrollY / sizes.height

The camera is now going down of 1 unit for each section scrolled. But the objects are currently separated by 4 units which is the objectsDistance variable:

We need to multiply the value by objectsDistance:

camera.position.y = - scrollY / sizes.height * objectsDistance

To put it in a nutshell, if the user scrolls down one section, then the camera will move down to the next object:

Position object horizontally

Now is a good time to position the objects left and right to match the titles:

mesh1.position.x = 2
mesh2.position.x = - 2
mesh3.position.x = 2

Parallax

We call parallax the action of seeing one object through different observation points. This is done naturally by our eyes and it’s how we feel the depth of things.

To make our experience more immersive, we are going to apply this parallax effect by making the camera move horizontally and vertically according to the mouse movements. It will create a natural interaction, and help the user feel the depth.

Cursor

First, we need to retrieve the cursor position.

To do that, create a cursor object with x and y properties:

/**
 * Cursor
 */
const cursor = {}
cursor.x = 0
cursor.y = 0

Then, listen to the mousemove event on window and update those values:

window.addEventListener('mousemove', (event) =>
{
    cursor.x = event.clientX
    cursor.y = event.clientY

    console.log(cursor)
})

You should get the pixel positions of the cursor in the console:

While we could use those values directly, it’s always better to adapt them to the context.

First, the amplitude depends on the size of the viewport and users with different screen resolutions will have different results. We can normalize the value (from 0 to 1) by dividing them by the size of the viewport:

window.addEventListener('mousemove', (event) =>
{
    cursor.x = event.clientX / sizes.width
    cursor.y = event.clientY / sizes.height

    console.log(cursor)
})

While this is better already, we can do even more.

We know that the camera will be able to go as much on the left as on the right. This is why, instead of a value going from 0 to 1 it’s better to have a value going from -0.5 to 0.5.

To do that, subtract 0.5:

window.addEventListener('mousemove', (event) =>
{
    cursor.x = event.clientX / sizes.width - 0.5
    cursor.y = event.clientY / sizes.height - 0.5

    console.log(cursor)
})

Here is a clean value adapted to the context:

Remove the console.log.

We can now use the cursor values in the tick function. Create a parallaxX and a parallaxY variable and put the cursor.x and cursor.y in them:

const tick = () =>
{
    // ...

    // Animate camera
    camera.position.y = - scrollY / sizes.height * objectsDistance

    const parallaxX = cursor.x
    const parallaxY = cursor.y
    camera.position.x = parallaxX
    camera.position.y = parallaxY

    // ...
}

Unfortunately, we have two issues.

The x and y axes don’t seem synchronized in terms of direction. And, the camera scroll doesn’t work anymore:

Let’s fix the first issue. When we move the cursor to the left, the camera seems to go to the left. Same thing for the right. But when we move the cursor up, the camera seems to move down and the opposite when moving the cursor down.

To fix that weird feeling, invert the cursor.y:

    const parallaxX = cursor.x
    const parallaxY = - cursor.y
    camera.position.x = parallaxX
    camera.position.y = parallaxY

For the second issue, the problem is that we update the camera.position.y twice and the second one will replace the first one.

To fix that, we are going to put the camera in a Group and apply the parallax on the group and not the camera itself.

Right before instantiating the camera, create the Group, add it to the scene and add the camera to the Group:

/**
 * Camera
 */
// Group
const cameraGroup = new THREE.Group()
scene.add(cameraGroup)

// Base camera
const camera = new THREE.PerspectiveCamera(35, sizes.width / sizes.height, 0.1, 100)
camera.position.z = 6
cameraGroup.add(camera)

This shouldn’t change the result, but now, the camera is inside a group.

In the tick function, instead of applying the parallax on the camera, apply it on the cameraGroup:

const tick = () =>
{
    // ...

    // Animate camera
    camera.position.y = - scrollY / sizes.height * objectsDistance

    const parallaxX = cursor.x
    const parallaxY = - cursor.y
    
    cameraGroup.position.x = parallaxX
    cameraGroup.position.y = parallaxY

    // ...
}

The scroll animation and parallax animation are now mixed together nicely:

But we can do even better.

Easing

The parallax animation is a good start, but it feels a bit too mechanic. Having such a linear animation is impossible in real life for a number of reasons: the camera has weight, there is friction with the air and surfaces, muscles can’t make such a linear movement, etc. This is why the movement feels a bit wrong. We are going to add some “easing” (also called “smoothing” or “lerping”) and we are going to use a well-known formula.

The idea behind the formula is that, on each frame, instead of moving the camera straight to the target, we are going to move it (let’s say) a 10th closer to the destination. Then, on the next frame, another 10th closer. Then, on the next frame, another 10th closer.

On each frame, the camera will get a little closer to the destination. But, the closer it gets, the slower it moves because it’s always a 10th of the actual position toward the target position.

First, we need to change the = to += because we are adding to the actual position:

    cameraGroup.position.x += parallaxX
    cameraGroup.position.y += parallaxY

Then, we need to calculate the distance from the actual position to the destination:

    cameraGroup.position.x += (parallaxX - cameraGroup.position.x)
    cameraGroup.position.y += (parallaxY - cameraGroup.position.y)

Finally, we only want a 10th of that distance:

    cameraGroup.position.x += (parallaxX - cameraGroup.position.x) * 0.1
    cameraGroup.position.y += (parallaxY - cameraGroup.position.y) * 0.1

The animation feels a lot smoother:

But there is still a problem that some of you might have noticed.

If you test the experience on a high frequency screen, the tick function will be called more often and the camera will move faster toward the target. While this is not a big issue, it’s not accurate and it’s preferable to have the same result across devices as much as possible.

To fix that, we need to use the time spent between each frame.

Right after instantiating the Clock, create a previousTime variable:

const clock = new THREE.Clock()
let previousTime = 0

At the beginning of the tick function, right after setting the elapsedTime, calculate the deltaTime by subtracting the previousTime from the elapsedTime:

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()
    const deltaTime = elapsedTime - previousTime

    // ...
}

And then, update the previousTime to be used on the next frame:

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()
    const deltaTime = elapsedTime - previousTime
    previousTime = elapsedTime

    console.log(deltaTime)

    // ...
}

You now have the time spent between the current frame and the previous frame in seconds. For high frequency screens, the value will be smaller because less time was needed.

We can now use that deltaTime on the parallax, but, because the deltaTime is in seconds, the value will be very small (around 0.016 for most common screens running at 60fps). Consequently, the effect will be very slow.

To fix that, we can change 0.1 to something like 5:

    cameraGroup.position.x += (parallaxX - cameraGroup.position.x) * 5 * deltaTime
    cameraGroup.position.y += (parallaxY - cameraGroup.position.y) * 5 * deltaTime

We now have a nice easing that will feel the same across different screen frequencies:

Finally, now that we have the animation set properly, we can lower the amplitude of the effect:

    const parallaxX = cursor.x * 0.5
    const parallaxY = - cursor.y * 0.5

Particles

A good way to make the experience more immersive and to help the user feel the depth is to add particles.

We are going to create very simple square particles and spread them around the scene.

Because we need to position the particles ourselves, we are going to create a custom BufferGeometry.

Create a particlesCount variable and a positions variable using a Float32Array:

/**
 * Particles
 */
// Geometry
const particlesCount = 200
const positions = new Float32Array(particlesCount * 3)

Create a loop and add random coordinates to the positions array:

for(let i = 0; i < particlesCount; i++)
{
    positions[i * 3 + 0] = Math.random()
    positions[i * 3 + 1] = Math.random()
    positions[i * 3 + 2] = Math.random()
}

We will change the positions later, but for now, let’s keep things simple and make sure that our geometry is working.

Instantiate the BufferGeometry and set the position attribute:

const particlesGeometry = new THREE.BufferGeometry()
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))

Create the material using PointsMaterial:

// Material
const particlesMaterial = new THREE.PointsMaterial({
    color: '#ffeded',
    sizeAttenuation: true,
    size: 0.03
})

Create the particles using Points:

// Points
const particles = new THREE.Points(particlesGeometry, particlesMaterial)
scene.add(particles)

You should get a bunch of particles spread around in a cube:

We can now position the particles on the three axes.

For the x (horizontal) and z (depth), we can use random values that can be as much positive as they are negative:

for(let i = 0; i < particlesCount; i++)
{
    positions[i * 3 + 0] = (Math.random() - 0.5) * 10
    positions[i * 3 + 1] = Math.random()
    positions[i * 3 + 2] = (Math.random() - 0.5) * 10
}

For the y (vertical) it’s a bit more tricky. We need to make the particles start high enough and then spread far enough below so that we reach the end with the scroll.

To do that, we can use the objectsDistance variable and multiply by the number of objects which is the length of the sectionMeshes array:

for(let i = 0; i < particlesCount; i++)
{
    positions[i * 3 + 0] = (Math.random() - 0.5) * 10
    positions[i * 3 + 1] = objectsDistance * 0.5 - Math.random() * objectsDistance * sectionMeshes.length
    positions[i * 3 + 2] = (Math.random() - 0.5) * 10
}

That’s all for the particles, but you can improve them with random sizes, random alpha. And, we can even animate them.

Triggered rotations

As a final feature and to make the exercise just a bit harder, we are going to make the objects do a little spin when we arrive at the corresponding section in addition to the permanent rotation.

Knowing when to trigger the animation

First, we need a way to know when we reach a section. There are plenty of ways of doing that and we could even use a library, but in our case, we can use the scrollY value and do some math to find the current section.

After creating the scrollY variable, create a currentSection variable and set it to 0:

let scrollY = window.scrollY
let currentSection = 0

In the 'scroll' event callback function, calculate the current section by dividing the scrollY by sizes.height:

window.addEventListener('scroll', () =>
{
    scrollY = window.scrollY

    const newSection = scrollY / sizes.height
    
    console.log(newSection)
})

This works because each section is exactly one height of the viewport.

To get the exact section instead of that float value, we can use Math.round():

window.addEventListener('scroll', () =>
{
    scrollY = window.scrollY

    const newSection = Math.round(scrollY / sizes.height)
    
    console.log(newSection)
})

We can now test if newSection is different from currentSection. If so, that means we changed the section and we can update the currentSection in order to do our animation:

window.addEventListener('scroll', () =>
{
    scrollY = window.scrollY
    const newSection = Math.round(scrollY / sizes.height)

    if(newSection != currentSection)
    {
        currentSection = newSection

        console.log('changed', currentSection)
    }
})

Animating the meshes

We can now animate the meshes and, to do that, we are going to use GSAP.

The GSAP library is already loaded from the HTML file as we did for Three.js.

Then, in the if statement we did earlier, we can do the animation with gsap.to():

window.addEventListener('scroll', () =>
{
    // ...
    
    if(newSection != currentSection)
    {
        // ...

        gsap.to(
            sectionMeshes[currentSection].rotation,
            {
                duration: 1.5,
                ease: 'power2.inOut',
                x: '+=6',
                y: '+=3'
            }
        )
    }
})

While this code is valid, it will unfortunately not work. The reason is that, on each frame, we are already updating the rotation.x and rotation.y of each mesh with the elapsedTime.

To fix that, in the tick function, instead of setting a very specific rotation based on the elapsedTime, we are going to add the deltaTime to the current rotation:

const tick = () =>
{
    // ...

    for(const mesh of sectionMeshes)
    {
        mesh.rotation.x += deltaTime * 0.1
        mesh.rotation.y += deltaTime * 0.12
    }

    // ...
}

Final code

You can download the final project here https://threejs-journey.com/resources/codrops/threejs-scroll-based-animation/final.zip

Go further

We kept things really simple on purpose, but you can for sure go much further!

  • Add more content to the HTML
  • Animate other properties like the material
  • Animate the HTML texts
  • Improve the particles
  • Add more tweaks to the Debug UI
  • Test other colors
  • Add mobile and touch support
  • Etc.

If you liked this tutorial or want to learn more about WebGL and Three.js, join the Three.js Journey course!

As a reminder, here’s a 20% discount CODROPS1 for you 😉

The post Crafting Scroll Based Animations in Three.js appeared first on Codrops.