Front End Web Development

[David Baron’s] Thoughts on an implementable path forward for Container Queries

Css Tricks - Wed, 04/29/2020 - 5:50am

That’s the title of a public post from David Baron, a Principal Engineer at Firefox, with thoughts toward container queries. I know a lot of people have been holding their breath waiting for David’s ideas, as he’s one of few uniquely qualified to understand the ins and outs of this and speak to implementation possibility.

We’re still in the early stages of container queries. Every web designer and developer wants them, the browsers know it, but it’s a super complicated situation. It was very encouraging in February 2020 to hear positive signals about a possible switch-statement syntax that would give us access to an available-inline-size used to conditionally set individual values.

Now we’re seeing a second idea that is also in the realm of the possible.

This ideas uses an @rule instead for the syntax. From the document:

@container <selector> (<container-media-query>)? { // ... rules ... }

So I’m imagining it like:

.parent { contain: layout inline-size; display: grid; grid-template-columns: 100%; gap: 1rem; } @container .parent (min-width: 400px) { grid-template-columns: 1fr 1fr; .child::before { content: "Hello from container query land!"; } }

Except…

  1. I’m not sure if you’d have to repeat the selector inside as well? Or if dropping property/value pairs in there automatically applies to the selector in the @rule.
  2. David says, “The rules can match only that container’s descendants. Probably we’d need support for some properties applying to the container itself, but others definitely can’t.” I’d hope grid properties are a strong contender for something you can change, but I have no idea. Otherwise, I think we’d see people wrapping elements with <div class="container-query"> to get around the “only descendants” limit.

Containment seems to be a very important part of this. Like, if the element isn’t property contained, the container query just won’t work. I don’t know that much about containment, but Rachel has a great deep dive from late last year.

Again, this is super early days, I’m just having fun watching this and none of us really have any idea what will actually make it to browsers.

Direct Link to ArticlePermalink

The post [David Baron’s] Thoughts on an implementable path forward for Container Queries appeared first on CSS-Tricks.

Alpine.js: The JavaScript Framework That’s Used Like jQuery, Written Like Vue, and Inspired by TailwindCSS

Css Tricks - Wed, 04/29/2020 - 4:50am

We have big JavaScript frameworks that tons of people already use and like, including React, Vue, Angular, and Svelte. Do we need another JavaScript library? Let’s take a look at Alpine.js and you can decide for yourself. Alpine.js is for developers who aren’t looking to build a single page application (SPA). It’s lightweight (~7kB gzipped) and designed to write markup-driven client-side JavaScript.

The syntax is borrowed from Vue and Angular directive. That means it will feel familiar if you’ve worked with those before. But, again, Alpine.js is not designed to build SPAs, but rather enhance your templates with a little bit of JavaScript.

For example, here’s an Alpine.js demo of an interactive “alert” component.

CodePen Embed Fallback

The alert message is two-way bound to the input using x-model="msg". The “level” of the alert message is set using a reactive level property. The alert displays when when both msg and level have a value.

It’s like a replacement for jQuery and JavaScript, but with declarative rendering

Alpine.js is a Vue template-flavored replacement for jQuery and vanilla JavaScript rather than a React/Vue/Svelte/WhateverFramework competitor.

Since Alpine.js is less than a year old, it can make assumptions about DOM APIs that jQuery cannot. Let’s briefly draw a comparison between the two.

Querying vs. binding

The bulk of jQuery’s size and features comes in the shape of a cross-browser compatibility layer over imperative DOM APIs — this is usually referred to as jQuery Core and sports features that can query the DOM and manipulate it.

The Alpine.js answer to jQuery core is a declarative way to bind data to the DOM using the x-bind attribute binding directive. It can be used to bind any attribute to reactive data on the Alpine.js component. Alpine.js, like its declarative view library contemporaries (React, Vue), provides x-ref as an escape hatch to directly access DOM elements from JavaScript component code when binding is not sufficient (eg. when integrating a third-party library that needs to be passed a DOM Node).

Handling events

jQuery also provides a way to handle, create and trigger events. Alpine.js provides the x-on directive and the $event magic value which allows JavaScript functions to handle events. To trigger (custom) events, Alpine.js provides the $dispatch magic property which is a thin wrapper over the browser’s Event and Dispatch Event APIs.

Effects

One of jQuery’s key features is its effects, or rather, it’s ability to write easy animations. Where we might use slideUp, slideDown, fadeIn, fadeOut properties in jQuery to create effects, Alpine.js provides a set of x-transition directives, which add and remove classes throughout the element’s transition. That’s largely inspired by the Vue Transition API.

Also, jQuery’s Ajax client has no prescriptive solution in Alpine.js, thanks to the Fetch API or taking advantage of a third party HTTP library (e.g. axios, ky, superagent).

Plugins

It’s also worth calling out jQuery plugins. There is no comparison to that (yet) in the Alpine.js ecosystem. Sharing Alpine.js components is relatively simple, usually requiring a simple case of copy and paste. The JavaScript in Alpine.js components are “just functions” and tend not to access Alpine.js itself, making them relatively straightforward to share by including them on different pages with a script tag. Any magic properties are added when Alpine initializes or is passed into bindings, like $event in x-on bindings.

There are currently no examples of Alpine.js extensions, although there are a few issues and pull requests to add “core” events that hook into Alpine.js from other libraries. There are also discussions happening about the ability to add custom directives. The stance from Alpine.js creator Caleb Porzio, seems to be basing API decisions on the Vue APIs, so I would expect that any future extension point would be inspired on what Vue.js provides.

Size

Alpine.js is lighter weight than jQuery, coming in at 21.9kB minified — 7.1kB gzipped — compared to jQuery at 87.6kB minified — 30.4kB minified and gzipped. Only 23% the size!

Most of that is likely due to the way Alpine.js focuses on providing a declarative API for the DOM (e.g. attribute binding, event listeners and transitions).

Bundlephobia breaks down the two

For the sake of comparison, Vue comes in at 63.5kB minified (22.8kB gzipped). How can Alpine.js come in lighter despite it’s API being equivalent Vue? Alpine.js does not implement a Virtual DOM. Instead, it directly mutates the DOM while exposing the same declarative API as Vue.

Let’s look at an example

Alpine is compact because since application code is declarative in nature, and is declared via templates. For example, here’s a Pokemon search page using Alpine.js:

CodePen Embed Fallback

This example shows how a component is set up using x-data and a function that returns the initial component data, methods, and x-init to run that function on load.

Bindings and event listeners in Alpine.js with a syntax that’s strikingly similar to Vue templates.

  • Alpine: x-bind:attribute="express" and x-on:eventName="expression", shorthand is :attribute="expression" and @eventName="expression" respectively
  • Vue: v-bind:attribute="express" and v-on:eventName="expression", shorthand is :attribute="expression" and @eventName="expression" respectively

Rendering lists is achieved with x-for on a template element and conditional rendering with x-if on a template element.

Notice that Alpine.js doesn’t provide a full templating language, so there’s no interpolation syntax (e.g. {{ myValue }} in Vue.js, Handlebars and AngularJS). Instead, binding dynamic content is done with the x-text and x-html directives (which map directly to underlying calls to Node.innerText and Node.innerHTML).

An equivalent example using jQuery is an exercise you’re welcome to take on, but the classic style includes several steps:

  • Imperatively bind to the button click using $('button').click(/* callback */).
  • Within this “click callback” get the input value from the DOM, then use it to call the API.
  • Once the call has completed, the DOM is updated with new nodes generated from the API response.

If you’re interested in a side by side comparison of the same code in jQuery and Alpine.js, Alex Justesen created the same character counter in jQuery and in Alpine.js.

Back in vogue: HTML-centric tools

Alpine.js takes inspiration from TailwindCSS. The Alpine.js introduction on the repository is as “Tailwind for JavaScript.”

Why is that important?

One of Tailwind’s selling points is that it “provides low-level utility classes that let you build completely custom designs without ever leaving your HTML.” That’s exactly what Alpine does. It works inside HTML so there is no need to work inside of JavaScript templates the way we would in Vue or React  Many of the Alpine examples cited in the community don’t even use script tags at all!

Let’s look at one more example to drive the difference home. Here’s is an accessible navigation menu in Alpine.js that uses no script tags whatsoever.

CodePen Embed Fallback

This example leverages aria-labelledby and aria-controls outside of Alpine.js (with id references). Alpine.js makes sure the “toggle” element (which is a button), has an aria-expanded attribute that’s true when the navigation is expanded, and false when it’s collapsed. This aria-expanded binding is also applied to the menu itself and we show/hide the list of links in it by binding to hidden.

Being markup-centric means that Alpine.js and TailwindCSS examples are easy to share. All it takes is a copy-paste into HTML that is also running Alpine.js/TailwindCSS. No crazy directories full of templates that compile and render into HTML!

Since HTML is a fundamental building block of the web, it means that Alpine.js is ideal for augmenting server-rendered (Laravel, Rails, Django) or static sites (Hugo, Hexo, Jekyll). Integrating data with this sort of tooling can be a simple as outputting some JSON into the x-data="{}" binding. The affordance of passing some JSON from your backend/static site template straight into the Alpine.js component avoids building “yet another API endpoint” that simply serves a snippet of data required by a JavaScript widget.

Client-side without the build step

Alpine.js is designed to be used as a direct script include from a public CDN. Its developer experience is tailored for that. That’s why it makes for a great jQuery comparison and replacement: it’s dropped in and eliminates a build step.

While it’s not traditionally used this way, the bundled version of Vue can be linked up directly. Sarah Drasner has an excellent write-up showing examples of jQuery substituted with Vue. However, if you use Vue without a build step, you’re actively opting out of:

  • the Vue CLI
  • single file components
  • smaller/more optimized bundles
  • a strict CSP (Content Security Policy) since Vue inline templates evaluate expressions client-side

So, yes, while Vue boasts a buildless implementation, its developer experience is really depedent on the Vue CLI. That could be said about Create React App for React, and the Angular CLI. Going build-less strips those frameworks of their best qualities.

There you have it! Alpine.js is a modern, CDN-first  library that brings declarative rendering for a small payload — all without the build step and templates that other frameworks require. The result is an HTML-centric approach that not only resembles a modern-day jQuery but is a great substitute for it as well.

If you’re looking for a jQuery replacement that’s not going to force you into a SPAs architecture, then give Alpine.js a go! Interested? You can find out more on Alpine.js Weekly, a free weekly roundup of Alpine.js news and articles.

The post Alpine.js: The JavaScript Framework That’s Used Like jQuery, Written Like Vue, and Inspired by TailwindCSS appeared first on CSS-Tricks.

How to Redirect a Search Form to a Site-Scoped Google Search

Css Tricks - Tue, 04/28/2020 - 12:10pm

This is just a tiny little trick that might be helpful on a site where you don’t have the time or desire to build out a really good on-site search solution. Google.com itself can perform searches scoped to one particular site. The trick is getting people there using that special syntax without them even knowing it.

Make a search form:

<form action="https://google.com/search" target="_blank" type="GET"> <label> Search CSS-Tricks on Google: <input type="search" name="q"> </label> <input type="submit" value="Go"> </form>

When that form is submitted, we’ll intercept it and change the value to include the special syntax:

var form = document.querySelector("form"); form.addEventListener("submit", function (e) { e.preventDefault(); var search = form.querySelector("input[type=search]"); search.value = "site:css-tricks.com " + search.value; form.submit(); });

That’s all.

CodePen Embed Fallback

The post How to Redirect a Search Form to a Site-Scoped Google Search appeared first on CSS-Tricks.

Static or Not?

Css Tricks - Mon, 04/27/2020 - 11:32am

A quick opinion piece by Kev Quirk: Why I Don’t Use A Static Site Generator. Kev uses WordPress:

Want to blog on my iPad? I can. Want to do it on my phone? No problem. On a machine I don’t normally use? Not an issue, as long as it has a browser.

First, it’s worth understanding that by using WordPress it doesn’t opt you out of using a static site generator. WordPress has an API, and that opens the door to hit that API during a build process and build your site that way. That’s what Gatsby does, there is a plugin that exports a static site, and projects like Frontity really blur the lines.

But I agree with Kev here on his reasoning. For all his reasons, and 1,000 more, it’s a perfectly acceptable and often smart choice to run a WordPress site. I think about it in terms of robustness and feature-readiness. Need e-commerce? It’s there. Need forms? There are great plugins. Need to augment how the CMS works? You have control over the types of content and what is in them. Need auth? That’s a core feature. Wish you had a great editing experience? Gutenberg is glorious.

Time and time again, I build what I want to build with WordPress quickly and efficiently and it makes me feel productive and powerful. But I don’t wanna make this specifically about WordPress; this can be true of any “classic” CMS. Craft CMS has a GraphQL API out of the box. We just posted about a Drupal + Jamstack webinar.

In the relatively new world of static sites, a little thing can end up a journey of research and implementation, like you’re the only third person on Earth to ever do it.

Now all that said…

What do I think of static site generators and the Jamstack world? They are awesome.

I think there is a lot to be said about building sites this way. The decoupling of data and front-end is smart. The security is great. The DX, what with the deploy previews and git-based everything is great. The speed you get out of the gate is amazing (serving HTML from a CDN is some feat).

Just like a classic server-side CMS doesn’t opt you out of building a static site, building with a static site doesn’t opt you out of doing dynamic things — even super duper fancy dynamic things. Josh Comeau has a great new post going into this. He built a fancy little app that does a ton in the browser with React, but that doesn’t mean he still can’t deliver a good amount of it statically. He calls it a “mindset shift,” referring to the idea that you might think you need a database call, but do you really? Could that database call have already happened and generated a static file? And if not, still, some of it could have been generated with the last bits coming over dynamically.

I can’t wait for a world where we start really seeing the best of both worlds. We do as much statically as possible, we get whatever we can’t do that way with APIs, and we don’t compromise on the best tools along the way.

When to go with a static site…
  • If you can, you should consider it, as the speed and security can’t be beaten.
  • If you’re working with a Greenfield project.
  • If your project builds from and uses accessible APIs, you could hit that API during the build process as well as use it after the initial HTML loads.
  • If some static site generator looks like a perfect fit for something you’re doing.
  • If a cost analysis says it would be cheaper.
  • If functionality (like build previews) would be extremely helpful for a workflow.

When to go with server-side software…
  • If you need the features of a classic CMS (e.g. WordPress), and the technical debt of going static from there is too high.
  • If you’re already in deep with a server-rendered project (Ruby on Rails, Python, etc.) and don’t have any existing trouble.
  • If that is where you have the most team expertise.
  • If a cost analytics says it would be cheaper.
  • If there aren’t good static solutions around for what want to build (e.g. forums software).
  • If you have an extreme situation, like millions of URLs, and the build time for static is too high.

Bad reasons to avoid a static site…
  • You need to do things with servers. (Why? You can still hit APIs on servers, either at build or during runtime.)
  • You need auth. (Why? Jamstack is perfectly capable of auth with JWTs and such.)
  • You haven’t even looked into doing things Jamstack-style.
Bad reasons to choose server-side software…
  • You haven’t even looked into doing things Jamstack-style.
  • Because you think using comfortable / existing / classic / well-established / well-supported tools opt you out of building anything statically.
  • Something something SEO. (If anything, statically rendered content should perform better. But it’s understandable if a move to static means moving to client-side calls for something like product data.)

The post Static or Not? appeared first on CSS-Tricks.

Advice for Writing a Technical Resume

Css Tricks - Mon, 04/27/2020 - 4:50am

Marco Rogers asked a very good question on Twitter:

I talk to a lot of people new to tech from non-traditional backgrounds, e.g. bootcamps or self-taught. I'm looking for good information for those people on how to build out a strong resume when they don't have work experience yet. Advice is fine, links to resources is better.

— Marco Rogers (@polotek) April 10, 2020

I’ve been on both sides of the interview table for many years now, both searching for jobs and as a hiring manager. In my years of management, I’ve read though thousands of applications, this article is full of suggestions gleaned from that experience, should it be helpful to you.

When it comes to writing a resume, It’s helpful to think about the human aspect first and foremost. Imagining a hiring manager’s perspective will give you an edge because it helps speak to them directly. Remember, a coveted position at a reputable company commonly requires the team to sift through anywhere between tens and thousands of applications. Their staff is materially impacted in the time and energy it takes to review every candidate and evaluate those who make it in to the interview stage. Attention to details will help your odds of standing out in the crowd.

Here are my general suggestions to make the best possible resume.

Formatting is important

Spelling, grammar and formatting are all crucial to a well-written resume. Typos and errors are clear red flags, so please pay attention to what you write and how it is written. These types of mistakes give the impression that you either lack attention to detail or are unwilling to go the extra step. As trivial as this might seem, it will serve you well to use spell check and get a second set of eyes on your resume before submitting it.

A few formatting tips to keep in mind:

  • Use headings to separate sections
  • Use lists to help summarize highlights and keep things scannable
  • Use a good font and font size that makes the content legible
  • Use line spacing that lets content breathe rather than packing it close together
  • Make good use of bold and italic for pertinent information, but don’t abuse them

I don’t have a strong opinion on charts that show off your skills or lists of hobbies — though, I will say I’ve noticed them more frequently on the applications of beginners. You might unintentionally communicate you have less experience by including it.

If you don’t have a lot of work history, it’s totally OK to throw in open source projects!

Or side projects! Or working on your own site! A few folks mentioned this in the Twitter thread and it’s solid advice. A good hiring manager should know that Senior-level candidates don’t grow on trees — they want to see work that shows you have promise.

This is problematic advice in some ways, as not everyone has time on the side to devote to projects. Including these isn’t a hard requirement for a good resume, but if you’re otherwise lacking relevant work experience, including personal projects can show the kind of work you’re capable of doing as well as the kind of work that excites you. I’ve taken chances on folks with slim-to-no work experience but with a solid portfolio site, GitHub contributions, or even a few CodePen demos that show potential.

Call out your contributions in your work experience

Each time you list a work example, answer this: what did you accomplish? This is a good way to provide valuable information without unnecessary fluff.

Here’s an example that would catch my attention:

Due to my team’s work refactoring the product page, we were able to meet the demands of our customers, which resulted in a 25% growth in sales. We also took the opportunity to upgrade the codebase from React.createClass to React Hooks for all of our components, ensuring a more flexible and maintainable system.

This tells me you can work on a team to deliver goals. It also tells me that you understand technical debt and your part in being proactive in maintenance. That’s the sort of person I want to hire. Write the outcomes from the point of view of what you provided for the team.

If so far your experience is limited to a code bootcamp, it’s great to talk through that.

Every job applicant is coming from a different background and from varying degrees of experience. It’s safe to assume you are not the most experienced person in the pool.

And that’s OK!

For example, let’s say your development experience is limited to online or in-person coding bootcamps rather than commercial projects. What did you learn there? What were you interested in? What was your final project? Is there a link to that work? When I’m hiring someone who’s coming in early in their career, I’m mostly looking for curiosity and enthusiasm. I’m probably not alone there.

Don’t be too long… or too short

We mentioned earlier that hiring is a time-consuming job. It’s good to keep this in mind as you’re writing by making your resume as brief as possible — ideally on a single page. Two pages is OK if you really need it.

Keeping everything short is a balancing act when you’re also attempting to include as much useful information as possible. Treat that constraint as a challenge to focus on the most important details. It’s a good problem if you have more to say than what fits!

At best, padding a resume into multiple pages conveys you’re unable to communicate in a succinct manner. At worst, it shows a lack of respect for a hiring manager’s time.

Make sure there’s a way to reach you

I cannot tell you how many resumes that lack the following essentials: name, email, and phone number. Seriously, it happens even on resumes that are otherwise very impressive.

Your name and contact information are hard requirements. I don’t want to search around for your email if you’re applying. To be honest, I probably won’t search at all because I’m busy and there are many other candidates to choose from.

Preparation is your friend

Make sure your accompanying cover letter (yes, you should include one) communicates you’ve done at least a little research on the company, conveys you understand what they need in a candidate, and how you fit into that need.

I will personally adjust my the descriptions (not the job titles, but the takeaways and outcomes) in my own resume so there is a direct connection between my skills and the position.

Your work and education details should be reverse-chronological

Your most recent work is more important than your oldest work. It’s a better reflection of what you’re capable of doing today and how fresh your skills are in a particular area. The same goes for your education: lead with your most recent experience.

The person reviewing your resume can decide to continue reading further if they’re compelled by the most recent information.

Wrapping up

If you want to stand out in the crowd, make sure your resume is one that represents you well. Ask someone to help you proof and use spellcheck, and make sure you’ve put your best foot forward.

And don’t be discouraged by rejections or unreturned messages. It’s less likely to be about you personally and more likely due to the number of people applying. So keep trying!

The post Advice for Writing a Technical Resume appeared first on CSS-Tricks.

The Cost of Javascript Frameworks

Css Tricks - Sun, 04/26/2020 - 3:30am

I expect this post from Tim Kadlec to be quoted in every performance conference talk for the next few years. There is a lot of data here, so please check it out for yourself, but the short story is that JavaScript-framework-powered sites are definitely heavier and more resource-intensive than non-JavaScript-framework-powered sites. Angular is the beefiest and React is hardest on the CPU. But as Tim says:

… it says very little about the performance of the core frameworks in play and much more about the approach to development these frameworks may encourage

Another big caveat is that there isn’t data here on-site usage after first-load, which is a huge aspect of “single-page app” approaches.

Still, while you can be performant with frameworks (although even that top 10% isn’t super encouraging), the frameworks aren’t doing much to help what has turned into a bad situation. It mimics exactly what we talked about recently with accessibility. It’s not the frameworks “fault” exactly, but they are also the best positioned to stop the bleeding.

Direct Link to ArticlePermalink

The post The Cost of Javascript Frameworks appeared first on CSS-Tricks.

@property

Css Tricks - Sat, 04/25/2020 - 3:29am

The @property is totally new to me, but I see it’s headed to Chrome, so I suppose it’s good to know about!

There is a draft spec and an “intent to ship” document. The code from that document shows:

@property --my-property { syntax: "<color>"; initial-value: green; inherits: false; }

That is the CSS exact-equivalent to a   CSS.registerProperty(), the JavaScript syntax for declaring CSS custom properties, also a new thing (under the Houdini umbrella, it seems).

It looks like you declare these not within a selector block, but outside (like a @media query), and once you have, you haven’t actually created a new custom property yet, you’ve just registered the fact that you probably will later. When you actually go to create/use the custom property, you create it within a selector block like you already do now.

The “commonly cited use-case” is pretty darn cool. Right now, this isn’t possible in CSS:

.el { background: linear-gradient(white, black); /* this transition won't work */ transition: 1s; } .el:hover { background: linear-gradient(red, black); }

You might think the white in that gradient will fade to red with that transition, but no, that’s not transition-able in that way. If we needed this in the past, we’d resort to trickery like fading in a pseudo-element with the new gradient colors or transitioning the background-position of a wider-than-the-element gradient to fake it.

Sounds like now we can…

@property --gradient-start { syntax: "<color>"; initial-value: white; inherits: false; } .el { --gradient-start: white; background: linear-gradient(var(--gradient-start), black); transition: --gradient-start 1s; } .el:hover { --gradient-start: red; }

Presumably, that works now because we’ve told CSS that this custom property is a <color> so it can be treated/animated like a color in away that wasn’t possible before.

Reminds me of how when we use the attr() function to pull like data-size="22px" off an element, we can’t actually use the <length> 22px, it’s just a string. But that maybe-someday we’ll get attr(data-size px);

I have no idea when @property will actually be available, but looks like Chrome will ship first and there are positive signals from Safari and Firefox. &#x1f44d;

The post @property appeared first on CSS-Tricks.

How to Make a CSS-Only Carousel

Css Tricks - Fri, 04/24/2020 - 9:29am

We mentioned a way to make a CSS-only carousel in a recent issue of the newsletter and I thought that a more detailed write up would be interesting and capture some of my thoughts on making one.

So, here’s what we’re making today:

There’s no JavaScript here, whatsoever! No jQuery plugins. No trickiness. Just a couple of new-ish CSS properties that I’ve been experimenting with as well as some basic HTML.

OK to start, we need to focus on the markup. The design includes a left navigation made up of images and a large image gallery on the right that lets us scroll through each image individually. We’ll also need a wrapper to help us organize the layout:

<div class="wrapper"> <nav class="lil-nav"></nav> <div class="gallery"></div> </div>

Next, we can add images! For this little example, I checked out our list of sites with high quality images that you can use for free and went with Unsplash.

After saving images with the CodePen asset manager, I started adding the URLs to the nav element:

<nav class="lil-nav"> <a href="#image-1"> <img class="lil-nav__img" src="..." alt="Yosemite" /> </a> <a href="#image-2"> <img class="lil-nav__img" src="..." alt="Basketball hoop" /> </a> <!-- more images go here --> </nav>

See that the href to each of these links is pointing to an ID? That’s because if we look at the demo again, we want to be able to click an image and then we want to it to hop to the larger version of that image in the gallery to the right.

So, now we can start to add these images to the large gallery, too…

<div class="gallery"> <img class="gallery__img" id="image-1" src="..." alt="Yosemite" /> <img class="gallery__img" id="image-2" src="..." alt="Basketball hoop" /> <!-- more images go here --> </div>

Nifty. Next is the fun part: styling this bad boy. We can use a grid layout the parent .wrapper and set some smart defaults for the img element:

img { display: block; max-width: 100%; } .wrapper { display: grid; grid-template-columns: 1fr 5fr; grid-gap: 20px; } CodePen Embed Fallback

So far, we have our layout sorted and our links set up. Next, let’s account for overflow that might spill outside our wrapper and make sure that the nav and the gallery are scrollable:

.wrapper { display: grid; grid-template-columns: 1fr 5fr; grid-gap: 10px; overflow: hidden; height: 100vh; } .gallery { overflow: scroll; } .lil-nav { overflow-y: scroll; overflow-x: hidden; } CodePen Embed Fallback

We can scroll through each image in the gallery now, but if this was a production website we’d probably want to make sure that folks can scroll passed this carousel a bit more easily. Trent Walton wrote about this very problem several years ago and I think it’s always worth keeping in mind.

Next up, let’s focus on the carousel snap of each image in the gallery. To do that we’ll need to use the scroll-snap-type and scroll-snap-align property like this:

.gallery { overflow: scroll; scroll-snap-type: x mandatory; } .gallery__img { scroll-snap-align: start; margin-bottom: 10px; }

Now try scrolling through the gallery on the right-hand side again:

CodePen Embed Fallback

If you want to learn more about these properties I’d highly recommend this piece about practical CSS scroll snapping which digs into the nitty-gritty of these properties.

We have a pretty dang usable carousel! From here, all we have to do is tidy up the design because the gallery image isn’t the full height of the screen. To do that we can use object-fit and give each image a min-height with the vh unit, just like this:

.gallery__img { scroll-snap-align: start; margin-bottom: 10px; min-height: 100vh; object-fit: cover; }

Now the big gallery images will always be the full size of the screen and will scale to take up the width and height. Let’s move on and tackle the style of the little navigation images:

.lil-nav { overflow-y: scroll; overflow-x: hidden; } .lil-nav a { height: 200px; display: flex; margin-bottom: 10px; } .lil-nav__img { object-fit: cover; } CodePen Embed Fallback

At first, I made this little nav act like a carousel too, but it felt really weird. I’m just keeping the default scroll behavior for now. In that demo above, though, try clicking an image. Notice how it jumps to that image in the carousel immediately? It would be nice if we could animate that transition a bit — and we can!

.gallery { overflow: scroll; scroll-snap-type: x mandatory; scroll-behavior: smooth; }

That scroll-behavior CSS property is super handy for this and so now the whole thing will animate if you click one of the nav items:

CodePen Embed Fallback

Nifty, eh? One more tiny thing we could do here is throw a filter on the nav items to make them black and white and then animate them on hover:

.lil-nav__img { object-fit: cover; filter: saturate(0); transition: 0.3s ease all; } .lil-nav__img:hover { transform: scale(1.05); filter: saturate(1); }

I’m sure there’s a lot more we could do here but I think this works quite nicely!

CodePen Embed Fallback

We could even throw a tiny bit of JavaScript into the mix to show which image is active, but I reckon that folks know that just from looking at the gallery.

That’s it! We now have a carousel that’s pretty dang good for progressive enhancement and it means we don’t have to load a library of JavaScript or write a bunch more code than we really need to.

Making the carousel responsive…

Let’s go one step further though and make this chap responsive. What we want to do is reverse the order of our grid by moving all of our current styles into a media query that is only activated at larger screens.

You might want to open up this demo in a new tab and decrease/increase the size of the browser to see the changes take place:

CodePen Embed Fallback

If you load this demo on a mobile device you should see how the layout switches between the two modes. This is done by using a single media query on the .wrapper element. Note that we’re using Sass:

$large: 1200px; .wrapper { overflow: hidden; height: 100vh; display: grid; grid-template-rows: 2fr 1fr; grid-gap: 10px; @media screen and (min-width: $large) { grid-template-columns: 1fr 5fr; grid-template-rows: auto; } }

Let’s add one on the navigation, too. But this time, we need to tell the navigation to start on the second row so it moves to the bottom of the screen:

.lil-nav { overflow-x: scroll; overflow-y: hidden; display: flex; grid-row-start: 2; @media screen and (min-width: $large) { overflow-y: scroll; overflow-x: hidden; display: block; grid-row-start: auto; } }

With the gallery we need to switch around the scroll-type for larger screens and reverse the overflow property as well:

.gallery { overflow-x: scroll; overflow-y: hidden; scroll-snap-type: x mandatory; scroll-behavior: smooth; display: flex; @media screen and (min-width: $large) { display: block; overflow-y: scroll; overflow-x: hidden; scroll-snap-type: y mandatory; } }

That’s the bulk of the changes we’ve had to make and I quite like it! If we wanted to make this production-ready, we would think about accessibility (e.g. we probably don’t want screen readers to read out all the images in both the nav and gallery). Then there’s performance — we might consider lazy-loading so the images are only rendered when they’re needed.

Either way, this is a good start !

The post How to Make a CSS-Only Carousel appeared first on CSS-Tricks.

“The title ‘Front-End Developer’ is obsolete.”

Css Tricks - Fri, 04/24/2020 - 8:35am

That title is from the opening tweet of a thread from Benjamin De Cock. I wouldn’t go that far, myself. What I like about the term is that ‘Front-End’ literally means the browser, and while the job has been changing quite a lot — and is perhaps fracturing before our eyes — the fact that the job is about doing browser work is still true. We’re browser people. This was a point I tried to make in my “Ooooops I guess we’re full-stack developers now” talk.

I really like Benjamin’s sentiment though. There is a scourge of implementations of things on the web that are both heavier and worse because they re-implement something that the browser offers better and “for free.” Think sliders: scrolling behavior, snap points, fixed/sticky positioning, form controls, animation, etc.

Our industry seems to have acknowledged that backend and frontend developers require very different skills (even though they often use the exact same language), and yet it’s struggling to see there’s too much bundled into the term “front-end developer”.

That’s the tricky part. That’s at the heart of The Great Divide. There’s an awful lot of front-end developers where their job solely focuses on JavaScript. You could call them “JavaScript Engineers” or “JavaScript Developers,” and that feels OK. However, I’m not sure what you call someone who’s a great front-end developer, not particularly focused on JavaScript, but is on other aspects of the front end.

The modern frontend developer is most often than not a “Jack of all trades” mastering JS (or even just a framework) and barely tolerating HTML/CSS as a necessary evil. That’s understandable. I strongly think it’s a different specialization, and it’s too much for a single person.

Yep, it’s OK! The divide isn’t a bad thing; it’s just a thing. Front-end teams need JavaScript specialists and CSS specialists and accessibility specialists and performance specialists and animation specialists and internationalization specialists and, and, and, and. They don’t have to all be separate people. People can be good at multiple things. It’s just exceptionally rare that people are good at everything, even when scoped only to front-end skills.

The post “The title ‘Front-End Developer’ is obsolete.” appeared first on CSS-Tricks.

Chrome + System Fonts Snafu

Css Tricks - Fri, 04/24/2020 - 5:49am

There was just a bug late last year where system fonts (at least on Mac, I don’t know what the story was on other platforms) in Chrome appeared too thin and tracked-in at small sizes and too thick and tracked-out at larger sizes. That one was fixed, thankfully. But while it was a problem, it was the reason I gave up on system fonts for now and switched something else. A performance loss but aesthetic gain.

Now there is a new much worse bug, where the system font can’t be bolded. It’s not great, as a ton of sites roll with the system font stack as it has two major benefits: 1) it can help your site look like the operating system 2) it has great performance as the site doesn’t need to download/display and custom fonts.

Jon Henshaw wrote it up:

… the bug caught the attention of Adam Argyle, maker of VisBug and Chrome CSS Developer Advocate at Google. Argyle created a Chromium bug report, but the Chromium development team ultimately decided it wasn’t a blocker for releasing version 81. That resulted in sites like Coywolf not being able to use bold text for fonts that are larger than 16px (e.g., every heading).

The bug won’t be fixed in version 82 because the Chromium team announced that they’re skipping it, and will be releasing version 83 in mid-May instead. Argyle assured everyone on the original GitHub bug report that it would be fixed in version 83.

Above is Jon’s site. Andy Bell’s site got hit by it too.

So we’re looking at 4 weeks or so. Šime Vidas proposed a temporary fix of going Helvetica for now:

body { font-family: -apple-system, Helvetica; }

I guess with -apple-system in there, older versions of Chrome/macOS still might be able to benefit from system fonts? Not sure.

That brings up a source of confusion for me. When I first heard of using system font stacks, there was -apple-system and BlinkMacSystemFont and you were supposed to use them in that order in the font stack. Then came along -system-ui, and that seemed to work well all by itself and that was nice as it was obviously less Mac-specific. But there is also system-ui (no starting dash), and that seems to do the same thing and I’m not sure which is correct. Now it looks like the plan is ui-sans-serif and friends (like ui-serif and ui-monospace). I like the idea, but I’d love to hear clarity from browser vendors on what the recommended use is. Are we in a spot like this?

/* Just a guess... */ body { font-family: ui-sans-serif, system-ui, -system-ui, -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji"; }

Another observation from me… as I was trying to replicate this on Chrome 81, at first I was like “weird, works for me”, because I was trying out the bolding on default 16px text. I noticed that it was when the font was 20px or bigger the problem kicked in:

Bramus has an alternative fix idea: use Inter.

The post Chrome + System Fonts Snafu appeared first on CSS-Tricks.

SVG, Favicons, and All the Fun Things We Can Do With Them

Css Tricks - Fri, 04/24/2020 - 4:58am

Favicons are the little icons you see in your browser tab. They help you understand which site is which when you’re scanning through your browser’s bookmarks and open tabs. They’re a neat part of internet history that are capable of performing some cool tricks.

One very new trick is the ability to use SVG as a favicon. It’s something that most modern browsers support, with more support on the way.

Here’s the code for how to add favicons to your site:

<link rel="icon" type="image/svg+xml" href="/favicon.svg"> <link rel="alternate icon" href="/favicon.ico"> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#ff8a01">

If a browser doesn’t support a SVG favicon, it will ignore the first link element declaration and continue on to the second. This ensures that all browsers that support favicons can enjoy the experience. 

You may also notice the alternate attribute value for our rel declaration in the second line. This programmatically communicates to the browser that the favicon with a file format that uses .ico is specifically used as an alternate presentation.

Following the favicons is a line of code that loads another SVG image, one called safari-pinned-tab.svg. This is to support Safari’s pinned tab functionality, which existed before other browsers had SVG favicon support. There’s additional files you can add here to enhance your site for different apps and services, but more on that in a bit.

Here’s more detail on the current level of SVG favicon support:

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

DesktopChromeFirefoxIEEdgeSafari8041No80TPMobile / TabletAndroid ChromeAndroid FirefoxAndroidiOS Safari81NoNo13.4 Why SVG?

You may be questioning why this is needed. The .ico file format has been around forever and can support images up to 256×256 pixels in size. Here are three answers for you.

Ease of authoring

It’s a pain to make .ico files. The file is a proprietary format used by Microsoft, meaning you’ll need specialized tools to make them. SVG is an open standard, meaning you can use them without any further tooling or platform lock-in.

Future-proofing

Retina? 5k? 6k? When we use a resolution-agnostic SVG file for a favicon, we guarantee that our favicons look crisp on future devices, regardless of how large their displays get

Performance

SVGs are usually very small files, especially when compared to their raster image counterparts — even more-so if you optimize them beforehand. By only using a 16×16 pixel favicon as a fallback for browsers that don’t support SVG, we provide a combination that enjoys a high degree of support with a smaller file size to boot. 

This might seem a bit extreme, but when it comes to web performance, every byte counts!

Tricks

Another cool thing about SVG is we can embed CSS directly in it. This means we can do fun things like dynamically adjust them with JavaScript, provided the SVG is declared inline and not embedded using an img element.

<svg  version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">   <style>     path { fill: #272019; }   </style>   <!-- etc. --> </svg>

Since SVG favicons are embedded using the link element, they can’t really be modified using JavaScript. We can, however, use things like emoji and media queries.

Emoji

Lea Verou had a genius idea about using emoji inside of SVG’s text element to make a quick favicon with a transparent background that holds up at small sizes.

https://twitter.com/LeaVerou/status/1241619866475474946

In response, Chris Coyier whipped up a neat little demo that lets you play around with the concept.

Dark Mode support

Both Thomas Steiner and Mathias Bynens independently stumbled across the idea that you can use the prefers-color-scheme media query to provide support for dark mode. This work is built off of Jake Archibald’s exploration of SVG and media queries.

<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg">   <style>     path { fill: #000000; }     @media (prefers-color-scheme: dark) {       path { fill: #ffffff; }     }   </style>   <path d="M111.904 52.937a1.95 1.95 0 00-1.555-1.314l-30.835-4.502-13.786-28.136c-.653-1.313-2.803-1.313-3.456 0L48.486 47.121l-30.835 4.502a1.95 1.95 0 00-1.555 1.314 1.952 1.952 0 00.48 1.99l22.33 21.894-5.28 30.918c-.115.715.173 1.45.768 1.894a1.904 1.904 0 002.016.135L64 95.178l27.59 14.59c.269.155.576.232.883.232a1.98 1.98 0 001.133-.367 1.974 1.974 0 00.768-1.894l-5.28-30.918 22.33-21.893c.518-.522.71-1.276.48-1.99z" fill-rule="nonzero"/> </svg>

For supporting browsers, this code means our star-shaped SVG favicon will change its fill color from black to white when dark mode is activated. Pretty neat!

Other media queries

Dark mode support got me thinking: if SVGs can support prefers-color-scheme, what other things can we do with them? While the support for Level 5 Media Queries may not be there yet, here’s some ideas to consider:

A mockup of how these media query-based adjustments could work. Keep it crisp

Another important aspect of good favicon design is making sure they look good in the small browser tab area. The secret to this is making the paths of the vector image line up to the pixel grid, the guide a computer uses to turn SVG math into the bitmap we see on a screen. 

Here’s a simplified example using a square shape:

When the vector points of the square align to the pixel grid of the artboard, the antialiasing effect a computer uses to smooth out the shapes isn’t needed. When the vector points aren’t aligned, we get a “smearing” effect:

A vector point’s position can be adjusted on the pixel grid by using a vector editing program such as Figma, Sketch, Inkscape, or Illustrator. These programs export SVGs as well. To adjust a vector point’s location, select each node with a precision selection tool and drag it into position.

Some more complicated icons may need to be simplified, in order to look good at such a small size. If you’re looking for a good primer on this, Jeremy Frank wrote a really good two-part article over at Vidget.

Go the extra mile

In addition to favicons, there are a bunch of different (and unfortunately proprietary) ways to use icons to enhance its experience. These include things like the aforementioned pinned tab icon for Safari¹, chat app unfurls, a pinned Windows start menu tile, social media previews, and homescreen launchers.

If you’re looking for a great place to get started with these kinds of enhancements, I really like realfavicongenerator.net.

It’s a lot, but it guarantees robust support.

A funny thing about the history of the favicon: Internet Explorer was the first browser to support them and they were snuck in at the 11th hour by a developer named Bharat Shyam:

As the story goes, late one night, Shyam was working on his new favicon feature. He called over junior project manager Ray Sun to take a look.

Shyam commented, “This is good, right? Check it in?”, requesting permission to check the code into the Internet Explorer codebase so it could be released in the next version. Sun didn’t think too much of it, the feature was cool and would clearly give IE an edge. So he told Shyam to go ahead and add it. And just like that, the favicon made its way into Internet Explorer 5, which would go on to become one of the largest browser releases the web has ever seen.

The next day, Sun was reprimanded by his manager for letting the feature get by so quickly. As it turns out, Shyam had specifically waited until later in the day, knowing that a less experienced Program Manager would give him a pass. But by then, the code had been merged in. Incidentally, you’d be surprised just how many relatively major browser features have snuck their way into releases like this.

From How We Got the Favicon by Jay Hoffmann

I’m happy to see the platform throw a little love at favicons. They’ve long been one of my favorite little design details, and I’m excited that they’re becoming more reactive to user’s needs. If you have a moment, why not sneak a SVG favicon into your project the same way Bharat Shyam did way back in 1999. 

¹ I haven’t been able to determine if Safari is going to implement SVG favicon support, but I hope they do. Has anyone heard anything?

The post SVG, Favicons, and All the Fun Things We Can Do With Them appeared first on CSS-Tricks.

Different Approaches to Responsive CSS Motion Path

Css Tricks - Fri, 04/24/2020 - 4:58am

As a follow-up to Jhey’s recent post on responsive motion paths, Michelle Barker notes that another approach could be to just transform: scale() the whole dang element.

The trade-off there is that you’re scaling both the path and the element on the path at the same time; Jhey’s approach only makes path flexbile and the element stays the same size.

Calculating scale is a really cool trick I think and one we’ve also covered before.

Direct Link to ArticlePermalink

The post Different Approaches to Responsive CSS Motion Path appeared first on CSS-Tricks.

Dark mode and variable fonts

Css Tricks - Thu, 04/23/2020 - 11:43am

Not so long ago, we wrote about dark mode in CSS and I’ve been thinking about how white text on a black background is pretty much always harder to read than black text on a white background. After thinking about this for a while, I realized that we can fix that problem by making the text thinner in dark mode using variable fonts!

Here’s an example of the problem where I’m using the typeface Yanone Kaffeesatz from Google Fonts. Notice that the section with white text on a black background looks heavier than the section with black text on a white background.

CodePen Embed Fallback

Oddly enough, these two bits of text are actually using the same font-weight value of 400. But to my eye, the white text looks extra bold on a black background.

Stare at this example for a while. This is just how white text looks on a darker background; it’s how our eyes perceive shapes and color. And this might not be a big issue in some cases but reading light text on a dark background is always way more difficult for readers. And if we don’t take care designing text in a dark mode context, then it can feel as if the text is vibrating as we read it.

How do we fix this?

Well, this is where variable fonts come in! We can use a lighter font weight to make the text easier to read whenever dark mode is active:

body { font-weight: 400; } @media (prefers-color-scheme: dark) { body { font-weight: 350; } }

Here’s how that looks with this new example:

CodePen Embed Fallback

This is better! The two variants now look a lot more balanced to me.

Again, it’s only a small difference, but all great designs consist of micro adjustments like this. And I reckon that, if you’re already using variable fonts and loading all these weights, then you should definitely adjust the text so it’s easier to read.

This effect is easier to spot if we compare the differences between longer paragraphs of text. Here we go, this time in Literata:

CodePen Embed Fallback

Notice that the text on the right feels bolder, but it just isn’t. It’s simply an optical allusion — both examples above have a font-weight of 500.

So to fix this issue we can do the same as the example above:

body { font-weight: 500; } @media (prefers-color-scheme: dark) { body { font-weight: 400; } } CodePen Embed Fallback

Again, it’s a slight change but it’s important because at these sizes every typographic improvement we make helps the reading experience.

Oh and here’s a quick Google fonts tip!

Google Fonts lets you add a font to your website by adding a <link> in the <head> of the document, like this:

<head> <link href="https://fonts.googleapis.com/css2?family=Rosario:wght@515&display=swap" rel="stylesheet"> </head>

That’s using the Rosario typeface and adding a font-weight of 515 — that’s the bit in the code above that says wght@515. Even if this happens to be a variable font, only this font weight will be downloaded. If we try to do something like this though…

body { font-weight: 400; }

…nothing will happen! In fact, the font won’t load at all. Instead, we need to declare which range of font-weight values we want by doing the following:

<link href="https://fonts.googleapis.com/css2?family=Yanone+Kaffeesatz:wght@300..500&display=swap" rel="stylesheet">

This @300..500 bit in the code above is what tells Google Fonts to download a font file with all the weights between 300 and 500. Alternatively, adding a ; between each weight will then only download weights 300 and 500 – so, for example, you can’t pick weight 301:

<link href="https://fonts.googleapis.com/css2?family=Yanone+Kaffeesatz:wght@300;500&display=swap" rel="stylesheet">

It took me a few minutes to figure out what was going wrong and why the font wasn’t loading at all, so hopefully the Google Fonts team can make that a bit clearer with the embed codes in the future. Perhaps there should be an option or a toggle somewhere to select a range or specific weights (or maybe I just didn’t see it).

Either way, I think all this is why variable fonts can be so gosh darn helpful; they allow us to adjust text in ways that we’ve never been able to do before. So, yay for variable fonts!

The post Dark mode and variable fonts appeared first on CSS-Tricks.

Innovating on Web Monetization: Coil and Firefox Reality

Css Tricks - Thu, 04/23/2020 - 4:56am

I still think Coil is cool. I have it installed on CSS-Tricks as a publisher and money trickles in. I have a paid account and I trickle out money to other sites that use it. I wrote about all that last year.

This’ll explode to something huge if we actually get the Web Monetization API stuff. No more browser extensions would be needed, and a real ecosystem could be built around it.

Anselm Hook writes about using Coil (for now) to monetize games on the web, which is a good reminder that this isn’t just for publications — it’s for anything-web. Coil even works for things off your own domain, like your YouTube channel.

Direct Link to ArticlePermalink

The post Innovating on Web Monetization: Coil and Firefox Reality appeared first on CSS-Tricks.

Rethinking Twitter as a Serverless App

Css Tricks - Thu, 04/23/2020 - 4:56am

In a previous article, we showed how to build a GraphQL API with FaunaDB. We’ve also written a series of articles [1, 2, 3, 4] explaining how traditional databases built for global scalability have to adopt eventual (vs. strong) consistency, and/or make compromises on relations and indexing possibilities. FaunaDB is different since it does not make these compromises. It’s built to scale so it can safely serve your future startup no matter how big it gets, without sacrificing relations and consistent data.

In this article, we’re very excited to start bringing all of this together in a real-world app with highly dynamic data in a serverless fashion using React hooks, FaunaDB, and Cloudinary. We will use the Fauna Query Language (FQL) instead of GraphQL and start with a frontend-only approach that directly accesses the serverless database FaunaDB for data storage, authentication, and authorization.


The golden standard for example applications that feature a specific technology is a todo app–mainly because they are simple. Any database out there can serve a very simple application and shine. 

And that is exactly why this app will be different! If we truly want to show how FaunaDB excels for real world applications, then we need to build something more advanced. 

Introducing Fwitter

When we started at Twitter, databases were bad. When we left, they were still bad

Evan Weaver

Since FaunaDB was developed by ex-Twitter engineers who experienced these limitations first-hand, a Twitter-like application felt like an appropriately sentimental choice. And, since we are building it with FaunaDB, let’s call this serverless baby ‘Fwitter’

Below is a short video that shows how it looks, and the full source code is available on GitHub.

When you clone the repo and start digging around, you might notice a plethora of well-commented example queries not covered in this article. That’s because we’ll be using Fwitter as our go-to example application in future articles, and building additional features into it with time. 

But, for now, here’s a basic rundown of what we’ll cover here:

We build these features without having to configure operations or set up servers for your database. Since both Cloudinary and FaunaDB are scalable and distributed out-of-the-box, we will never have to worry about setting up servers in multiple regions to achieve low latencies for users in other countries. 

Let’s dive in!

Modeling the data 

Before we can show how FaunaDB excels at relations, we need to cover the types of relations in our application’s data model. FaunaDB’s data entities are stored in documents, which are then stored in collections–like rows in tables. For example, each user’s details will be represented by a User document stored in a Users collection. And we eventually plan to support both single sign-on and password-based login methods for a single user, each of which will be represented as an Account document in an Accounts collection.

At this point, one user has one account, so it doesn’t matter which entity stores the reference (i.e., the user ID). We could have stored the user ID in either the Account or the User document in a one-to-one relation:

One-to-one

However, since one User will eventually have multiple Accounts (or authentication methods), we’ll have a one-to-many model.

One-to-many

In a one-to-many relation between Users and Accounts, each Account points to only one user, so it makes sense to store the User reference on the Account:

We also have many-to-many relations, like the relations between Fweets and Users, because of the complex ways users interact with each other via likes, comments, and refweets. 

Many-to-many

Further, we will use a third collection, Fweetstats, to store information about the interaction between a User and a Fweet.

Fweetstats’ data will help us determine, for example, whether or not to color the icons indicating to  the user that he has already liked, commented, or refweeted a Fweet. It also helps us determine what clicking on the heart means: unlike or like.

The final model for the application will look like this: 

The application model of the fwitter application

Fweets are the center of the model, because they contain the most important data of the Fweet such as the information about the message, the number of likes, refweets, comments, and the Cloudinary media that was attached. FaunaDB stores this data in a json format that looks like this: 

As shown in the model and in this example json, hashtags are stored as a list of references. If we wanted to, we could have stored the complete hashtag json in here, and that is the preferred solution in more limited document-based databases that lack relations. However, that would mean that our hashtags would be duplicated everywhere (as they are in more limited databases) and it would be more difficult to search for hashtags and/or retrieve Fweets for a specific hashtag as shown below.

Note that a Fweet does not contain a link to Comments, but the Comments collection contains a reference to the Fweet. That’s because one Comment belongs to one Fweet, but a Fweet can have many comments–similar to the one-to-many relation between Users and Accounts.

Finally, there is a FollowerStats collection which basically saves information about how much users interact with each other in order to personalize their respective feeds. We won’t cover that much in this article, but you can experiment with the queries in the source code and stay tuned for a future article on advanced indexing.

Hopefully, you’re starting to see why we chose something more complex than a ToDo app. Although Fwitter  is nowhere near the complexity of the real Twitter app on which it’s based, it’s already becoming apparent that implementing such an application without relations would be a serious brainbreaker. 

Now, if you haven’t already done so from the github repo, it’s finally time to get our project running locally!

Setup the project

To set up the project, go to the FaunaDB dashboard and sign up. Once you are in the dashboard, click on New Database, fill in a name, and click Save. You should now be on the “Overview” page of your new database. 

Next, we need a key that we will use in our setup scripts. Click on the Security tab in the left sidebar, then click the New key button. 

In the “New key” form, the current database should already be selected. For “Role”, leave it as “Admin”. Optionally, add a key name. Next, click Save and copy the key secret displayed on the next page. It will not be displayed again.

Now that you have your database secret, clone the git repository and follow the readme. We have prepared a few scripts so that you only have to run the following commands to initialize your app, create all collections, and populate your database. The scripts will give you further instructions:

// install node modules npm install // run setup, this will create all the resources in your database // provide the admin key when the script asks for it. // !!! the setup script will give you another key, this is a key // with almost no permissions that you need to place in your .env.local as the // script suggestions npm run setup npm run populate // start the frontend

After the script, your .env.local file should contain the bootstrap key that the script provided you (not the admin key)

REACT_APP_LOCAL___BOOTSTRAP_FAUNADB_KEY=<bootstrap key>

You can optionally create an account with Cloudinary and add your cloudname and a public template (there is a default template called ‘ml_default’ which you can make public) to the environment to include images and videos in the fweets. 

REACT_APP_LOCAL___CLOUDINARY_CLOUDNAME=<cloudinary cloudname> REACT_APP_LOCAL___CLOUDINARY_TEMPLATE=<cloudinary template>

Without these variables, the include media button will not work, but the rest of the app should run fine:

Creating the front end

For the frontend, we used Create React App to generate an application, then divided the application into pages and components. Pages are top-level components which have their own URLs. The Login and Register pages speak for themselves. Home is the standard feed of Fweets from the authors we follow; this is the page that we see when we log into our account. And the User and Tag pages show the Fweets for a specific user or tag in reverse chronological order. 

We use React Router to direct to these pages depending on the URL, as you can see in the src/app.js file.

<Router> <SessionProvider value={{ state, dispatch }}> <Layout> <Switch> <Route exact path="/accounts/login"> <Login /> </Route> <Route exact path="/accounts/register"> <Register /> </Route> <Route path="/users/:authorHandle" component={User} /> <Route path="/tags/:tag" component={Tag} /> <Route path="/"> <Home /> </Route> </Switch> </Layout> </SessionProvider> </Router>

The only other thing to note in the above snippet is the SessionProvider, which is a React context to store the user’s information upon login. We’ll revisit this in the authentication section. For now, it’s enough to know that this gives us access to the Account (and thus User) information from each component.

Take a quick look at the home page (src/pages/home.js) to see how we use a combination of hooks to manage our data. The bulk of our application’s logic is implemented in FaunaDB queries which live in the src/fauna/queries folder. All calls to the database pass through the query-manager, which in a future article, we’ll refactor into serverless function calls. But for now these calls originate from the frontend and we’ll secure the sensitive parts of it with FaunaDB’s ABAC security rules and User Defined Functions (UDF). Since FaunaDB behaves as a token-secured API, we do not have to worry about a limit on the amount of connections as we would in traditional databases. 

The FaunaDB JavaScript driver

Next, take a look at the src/fauna/query-manager.js file to see how we connect FaunaDB to our application using FaunaDB’s JavaScript driver, which is just a node module we pulled with `npm install`. As with any node module, we import it into our application as so:

import faunadb from 'faunadb'

And create a client by providing a token. 

this.client = new faunadb.Client({ secret: token || this.bootstrapToken })

We’ll cover tokens a little more in the Authentication section. For now, let’s create some data! 

Creating data

The logic to create a new Fweet document can be found in the src/fauna/queries/fweets.js file. FaunaDB documents are just like JSON, and each Fweet follows the same basic structure: 

const data = { data: { message: message, likes: 0, refweets: 0, comments: 0, created: Now() } }

The Now() function is used to insert the time of the query so that the Fweets in a user’s feed can be sorted chronologically. Note that FaunaDB automatically places timestamps on every database entity for temporal querying. However, the FaunaDB timestamp represents the time the document was last updated, not the time it was created, and the document gets updated every time a Fweet is liked; for our intended sorting order, we need the created time. 

Next, we send this data to FaunaDB with the Create() function. By providing Create() with the reference to the Fweets collection using Collection(‘fweets’), we specify where the data needs to go. 

const query = Create(Collection('fweets'), data )

We can now wrap this query in a function that takes a message parameter and executes it using client.query() which will send the query to the database. Only when we call client.query() will the query be sent to the database and executed. Before that, we combine as many FQL functions as we want to construct our query. 

function createFweet(message, hashtags) { const data = … const query = … return client.query(query) }

Note that we have used plain old JavaScript variables to compose this query and in essence just called functions. Writing FQL is all about function composition; you construct queries by combining small functions into larger expressions. This functional approach has very strong advantages. It allows us to use native language features such as JavaScript variables to compose queries, while also writing higher-order FQL functions that are protected from injection.

For example, in the query below, we add hashtags to the document with a CreateHashtags() function that we’ve defined elsewhere using FQL.

const data = { data: { // ... hashtags: CreateHashtags(tags), likes: 0, // ... }

The way FQL works from within the driver’s host language (in this case, JavaScript) is what makes FQL an eDSL (embedded domain-specific language). Functions like CreateHashtags() behave just like a native FQL function in that they are both just functions that take input. This means that we can easily extend the language with our own functions, like in this open source FQL library from the Fauna community. 

It’s also important to notice that we create two entities in two different collections, in one transaction. Thus, if/when things go wrong, there is no risk that the Fweet is created yet the Hashtags are not. In more technical terms, FaunaDB is transactional and consistent whether you run queries over multiple collections or not, a property that is rare in scalable distributed databases. 

Next, we need to add the author to the query. First, we can use the Identity() FQL function to return a reference to the currently logged in document. As discussed previously in the data modeling section, that document is of the type Account and is separated from Users to support SSO in a later phase.

Then, we need to wrap Identity() in a Get() to access the full Account document and not just the reference to it.

Get(Identity())

Finally, we wrap all of that in a Select() to select the data.user field from the account document and add it to the data JSON. 

const data = { data: { // ... hashtags: CreateHashtags(tags), author: Select(['data', 'user'], Get(Identity())), likes: 0, // ... } }

Now that we’ve constructed the query, let’s pull it all together and call client.query(query) to execute it.

function createFweet(message, hashtags) { const data = { data: { message: message, likes: 0, refweets: 0, comments: 0, author: Select(['data', 'user'], Get(Identity())), hashtags: CreateHashtags(tags), created: Now() } } const query = Create(Collection('fweets'), data ) return client.query(query) }

By using functional composition, you can easily combine all your advanced logic in one query that will be executed in one transaction. Check out the file src/fauna/queries/fweets.js to see the final result which takes even more advantage of function composition to add rate-limiting, etc. 

Securing your data with UDFs and ABAC roles

The attentive reader will have some thoughts about security by now. We are essentially creating queries in JavaScript and calling these queries from the frontend. What stops a malicious user from altering these queries? 

FaunaDB provides two features that allow us to secure our data: Attribute-Based Access Control (ABAC) and User Defined Functions (UDF). With ABAC, we can control which collections or entities that a specific key or token can access by writing Roles. 

With UDFs, we can push FQL statements to the database by using the CreateFunction(). 

CreateFunction({ name: 'create_fweet', body: <your FQL statement>, })

Once the function is in the database as a UDF, where the application can’t alter it anymore, we then call this UDF from the front end.

client.query( Call(Function('create_fweet'), message, hashTags) )

Since the query is now saved on the database (just like a stored procedure), the user can no longer manipulate it. 

One example of how UDFs can be used to secure a call is that we do not pass in the author of the Fweet. The author of the Fweet is derived from the Identity() function instead, which makes it impossible for a user to write a Fweet on someone’s behalf.

Of course, we still have to define that the user has access to call the UDF. For that, we will use a very simple ABAC role that defines a group of role members and their privileges. This role will be named logged_in_role, its membership will include all of the documents in the Accounts collection, and all of these members will be granted the privilege of calling the create_fweet UDF.

CreateRole( name: 'logged_in_role', privileges: [ { resource: q.Function('create_fweet'), actions: { call: true } } ], membership: [{ resource: Collection('accounts') }], )

We now know that these privileges are granted to an account, but how do we ‘become’ an Account? By using the FaunaDB Login() function to authenticate our users as explained in the next section.

How to implement authentication in FaunaDB

We just showed a role that gives Accounts the permissions to call the create_fweets function. But how do we “become” an Account?.

First, we create a new Account document, storing credentials alongside any other data associated with the Account (in this case, the email address and the reference to the User).

return Create(Collection('accounts'), { credentials: { password: password }, data: { email: email, user: Select(['ref'], Var('user')) } }) }

We can then call Login() on the Account reference, which retrieves a token.

Login( Match( < Account reference > , { password: password } ) )

We use this token in the client to impersonate the Account. Since all Accounts are members of the Account collection, this token fulfills the membership requirement of the logged_in_role and is granted access to call the create_fweet UDF.

To bootstrap this whole process, we have two very important roles.

  • bootstrap_role: can only call the login and register UDFs
  • logged_in_role: can call other functions such as create_fweet

The token you received when you ran the setup script is essentially a key created with the bootstrap_role. A client is created with that token in src/fauna/query-manager.js which will only be able to register or login. Once we log in, we use the new token returned from Login() to create a new FaunaDB client which now grants access to other UDF functions such as create_fweet. Logging out means we just revert to the bootstrap token. You can see this process in the src/fauna/query-manager.js, along with more complex role examples in the src/fauna/setup/roles.js file. 

How to implement the session in React

Previously, in the “Creating the front end” section, we mentioned the SessionProvider component. In React, providers belong to a React Context which is a concept to facilitate data sharing between different components. This is ideal for data such as user information that you need everywhere in your application. By inserting the SessionProvider in the HTML early on, we made sure that each component would have access to it. Now, the only thing a component has to do to access the user details is import the context and use React’s ‘useContext’ hook.

import SessionContext from '../context/session' import React, { useContext } from 'react' // In your component const sessionContext = useContext(SessionContext) const { user } = sessionContext.state

But how does the user end up in the context? When we included the SessionProvider, we passed in a value consisting of the current state and a dispatch function. 

const [state, dispatch] = React.useReducer(sessionReducer, { user: null }) // ... <SessionProvider value={{ state, dispatch }}>

The state is simply the current state, and the dispatch function is called to modify the context. This dispatch function is actually the core of the context since creating a context only involves calling React.createContext() which will give you access to a Provider and a Consumer.

const SessionContext = React.createContext({}) export const SessionProvider = SessionContext.Provider export const SessionConsumer = SessionContext.Consumer export default SessionContext

We can see that the state and dispatch are extracted from something that React calls a reducer (using React.useReducer), so let’s write a reducer.

export const sessionReducer = (state, action) => { switch (action.type) { case 'login': { return { user: action.data.user } } case 'register': { return { user: action.data.user } } case 'logout': { return { user: null } } default: { throw new Error(`Unhandled action type: ${action.type}`) } } }

This is the logic that allows you to change the context. In essence, it receives an action and decides how to modify the context given that action. In my case, the action is simply a type with a string. We use this context to keep user information, which means that we call it on a successful login with: 

sessionContext.dispatch({ type: 'login', data: e }) Adding Cloudinary for media 

When we created a Fweet, we did not take into account assets yet. FaunaDB is meant to store application data, not image blobs or video data. However, we can easily store the media on Cloudinary and just keep a link in FaunaDB. The following inserts the Cloudinary script (in app.js):

loadScript('https://widget.cloudinary.com/v2.0/global/all.js')

We then create a Cloudinary Upload Widget (in src/components/uploader.js):

window.cloudinary.createUploadWidget( { cloudName: process.env.REACT_APP_LOCAL___CLOUDINARY_CLOUDNAME, uploadPreset: process.env.REACT_APP_LOCAL___CLOUDINARY_TEMPLATE, }, (error, result) => { // ... } )

As mentioned earlier, you need to provide a Cloudinary cloud name and template in the environment variables (.env.local file) to use this feature. Creating a Cloudinary account is free and once you have an account you can grab the cloud name from the dashboard.

You have the option to use API keys as well to secure uploads. In this case, we upload straight from the front end so the upload uses a public template. To add a template or modify it to make it public, click on the gear icon in the top menu, go to Upload tab, and click Add upload preset

You could also edit the ml_default template and just make it public.

Now, we just call widget.open() when our media button is clicked.

const handleUploadClick = () => { widget.open() } return ( <div> <FontAwesomeIcon icon={faImage} onClick={handleUploadClick}></FontAwesomeIcon> </div> )

This provides us with a small media button that will open the Cloudinary Upload Widget when it’s clicked. 

When we create the widget, we can also provide styles and fonts to give it the look and feel of our own application as we did above (in src/components/uploader.js): 

const widget = window.cloudinary.createUploadWidget( { cloudName: process.env.REACT_APP_LOCAL___CLOUDINARY_CLOUDNAME, uploadPreset: process.env.REACT_APP_LOCAL___CLOUDINARY_TEMPLATE, styles: { palette: { window: '#E5E8EB', windowBorder: '#4A4A4A', tabIcon: '#000000', // ... }, fonts: {


Once we have uploaded media to Cloudinary, we receive a bunch of information about the uploaded media, which we then add to the data when we create a Fweet.

We can then simply use the stored id (which Cloudinary refers to as the publicId) with the Cloudinary React library (in src/components/asset.js):

import { Image, Video, Transformation } from 'cloudinary-react'

To show the image in our feed.

<div className="fweet-asset"> <Image publicId={asset.id} cloudName={cloudName} fetchFormat="auto" quality="auto" secure="true" /> </div>

When you use the id, instead of the direct URL, Cloudinary does a whole range of optimizations to deliver the media in the most optimal format possible. For example when you add a video image as follows:

<div className="fweet-asset"> <Video playsInline autoPlay loop={true} controls={true} cloudName={cloudName} publicId={publicId}> <Transformation width="600" fetchFormat="auto" crop="scale" /> </Video> </div>

Cloudinary will automatically scale down the video to a width of 600 pixels and deliver it as a WebM (VP9) to Chrome browsers (482 KB), an MP4 (HEVC) to Safari browsers (520 KB), or an MP4 (H.264) to browsers that support neither format (821 KB). Cloudinary does these optimizations server-side, significantly improving page load time and the overall user experience. 

Retrieving data

We have shown how to add data. Now we still need to retrieve data. Getting the data of our Fwitter feed has many challenges. We need to: 

  • Get fweets from people you follow in a specific order (taking time and popularity into account)
  • Get the author of the fweet to show his profile image and handle
  • Get the statistics to show how many likes, refweets and comments it has
  • Get the comments to list those beneath the fweet. 
  • Get info about whether you already liked, refweeted, or commented on this specific fweet. 
  • If it’s a refweet, get the original fweet. 

This kind of query fetches data from many different collections and requires advanced indexing/sorting, but let’s start off simple. How do we get the Fweets? We start off by getting a reference to the Fweets collection using the Collection() function.

Collection('fweets')

And we wrap that in the Documents() function to get all of the collection’s document references.

Documents(Collection('fweets'))

We then Paginate over these references.

Paginate(Documents(Collection('fweets')))

Paginate() requires some explanation. Before calling Paginate(), we had a query that returned a hypothetical set of data. Paginate() actually materializes that data into pages of entities that we can read. FaunaDB requires that we use this Paginate() function to protect us from writing inefficient queries that retrieve every document from a collection, because in a database built for massive scale, that collection could contain millions of documents. Without the safeguard of Paginate(), that could get very expensive!

Let’s save this partial query in a plain JavaScript variable references that we can continue to build on.

const references = Paginate(Documents(Collection('fweets')))

So far, our query only returns a list of references to our Fweets. To get the actual documents, we do exactly what we would do in JavaScript: map over the list with an anonymous function. In FQL, a Lambda is just an anonymous function.

const fweets = Map( references, Lambda(['ref'], Get(Var('ref'))) )

This might seem verbose if you’re used to declarative query languages like SQL that declare what you want and let the database figure out how to get it. In contrast, FQL declares both what you want and how you want it which makes it more procedural. Since you’re the one defining how you want your data, and not the query engine, the price and performance impact of your query is predictable. You can exactly determine how many reads this query costs without executing it, which is a significant advantage if your database contains a huge amount of data and is pay-as-you-go. So there might be a learning curve, but it’s well worth it in the money and hassle it will save you. And once you learn how FQL works, you will find that queries read just like regular code. 

Let’s prepare our query to be extended easily by introducing Let. Let will allow us to bind variables and reuse them immediately in the next variable binding, which allows you to structure your query more elegantly.

const fweets = Map( references, Lambda( ['ref'], Let( { fweet: Get(Var('ref')) }, // Just return the fweet for now Var('fweet') ) ) )

Now that we have this structure, getting extra data is easy. So let’s get the author.

const fweets = Map( references, Lambda( ['ref'], Let( { fweet: Get(Var('ref')), author: Get(Select(['data', 'author'], Var('fweet'))) }, { fweet: Var('fweet'), author: Var('user') } ) ) )

Although we did not write a join, we have just joined Users (the author) with the Fweets. We’ll expand on these building blocks even further in a follow up article. Meanwhile, browse src/fauna/queries/fweets.js to view the final query and several more examples. 

More in the code base

If you haven’t already, please open the code base for this Fwitter example app. You will find a plethora of well-commented examples we haven’t explored here, but will in future articles. This section touches on a few files we think you should check out.

First, check out the src/fauna/queries/fweets.js file for examples of how to do complex matching and sorting with FaunaDB’s indexes (the indexes are created in src/fauna/setup/fweets.js). We implemented three different access patterns to get Fweets by popularity and time, by handle, and by tag.

Getting Fweets by popularity and time is a particularly interesting access pattern because it actually sorts the Fweets by a sort of decaying popularity based on users’ interactions with each other. 

Also, check out src/fauna/queries/search.js, where we’ve implemented autocomplete based on FaunaDB indexes and index bindings to search for authors and tags. Since FaunaDB can index over multiple collections, we can write one index that supports an autocomplete type of search on both Users and Tags.

We’ve implemented these examples because the combination of flexible and powerful indexes with relations is rare for scalable distributed databases. Databases that lack relations and flexible indexes require you to know in advance how your data will be accessed and you will run into problems when your business logic needs to change to accommodate your clients’ evolving use cases.

In FaunaDB, if you did not foresee a specific way that you’d like to access your data, no worries — just add an Index! We have range indexes, term indexes, and composite indexes that can be specified whenever you want without having to code around eventual consistency. 

A preview of what’s to come 

As mentioned in the introduction, we’re introducing this Fwitter app to demonstrate complex, real-world use cases. That said, a few features are still missing and will be covered in future articles, including streaming, pagination, benchmarks, and a more advanced security model with short-lived tokens, JWT tokens, single sign-on (possibly using a service like Auth0), IP-based rate limiting (with Cloudflare workers), e-mail verification (with a service like SendGrid), and HttpOnly cookies.

The end result will be a stack that relies on services and serverless functions which is very similar to a dynamic JAMstack app, minus the static site generator. Stay tuned for the follow-up articles and make sure to subscribe to the Fauna blog and monitor CSS-Tricks for more FaunaDB-related articles. 

The post Rethinking Twitter as a Serverless App appeared first on CSS-Tricks.

My Visual Studio Code Setup: Extensions and Themes

Css Tricks - Wed, 04/22/2020 - 10:19am

Matthias Ott’s posted his VS Code setup. I find lists like this (I rounded up some recent updates of my own) irresistible, probably because, like y’all, I spend an awful lot of time in VS Code and wanna make sure I’m getting the most out of it.

Things from the list that stood out to me:

  • I didn’t realize Bracket Pair Colorizer had gone v2 and it’s a separate install.
  • I didn’t realize you needed an extension to honor .editorconfig files.
  • I wasn’t using anything for PHP, but Matthias listed PHP Intelephense and I’m giving it a whirl. It has fewer users than the non-weirdly named one though? And when I installed that, I saw Format HTML in PHP which I’m also trying because, yes, please! (Even Prettier’s PHP add-on can’t do that.)

Messing with extensions is also a good opportunity to clear out old crap.

Also super interesting…

So, https://t.co/7DZWDzAsTb is currently an email series (you can sign up now to catch up).

It's turning into an ebook/screencast/extra-stuff product.

I'm gonna cover A TON, but I think the PHP section will be one of the most valuable parts for most of you pic.twitter.com/sdVK4L1m4n

— Caleb Porzio (@calebporzio) April 20, 2020

The main point of that series is cleaning up the interface of VS Code in extreme ways. All the way down to:

Direct Link to ArticlePermalink

The post My Visual Studio Code Setup: Extensions and Themes appeared first on CSS-Tricks.

Puzzles and Mysteries

Css Tricks - Wed, 04/22/2020 - 4:54am

Bob Hoffman:

Puzzles, [Malcom Gladwell] wrote, are problems for which there is not enough information. An example of a puzzle: Where is Jimmy Hoffa buried? If we had more information, we would know the answer. If someone told us “Jimmy Hoffa is buried in New Jersey,” we’d know a little more than we know now. If they said, “He’s buried in northern New Jersey,” we’d know even more. If they said, “He’s buried in the Meadowlands,” we’d have an answer to our puzzle.

On the other hand, there are mysteries. Mysteries are problems for which we have plenty of information, but no accurate analysis. An example of a mystery: Why do inner-city schools do such a crappy job of educating kids? There are thousands of studies. Every education department of every university in America has done a study on this; every committee of Congress has done a report on it; every editorial writer has a theory about it, and every pundit has an opinion. And yet, we have no definitive answer.

It’s fun to think about how that correlates to front-end development. When we’re coding, every¹ problem we face is a puzzle. We just need more information and we can figure out what to do. Sometimes, design is like that too. Information can make our designs better. But success in design² has a nebulous quality that makes it feel more like a mystery.

  1. Except for CORS. CORS is a mystery.
  2. And business, marketing, and love.

Direct Link to ArticlePermalink

The post Puzzles and Mysteries appeared first on CSS-Tricks.

Fake Code

Css Tricks - Tue, 04/21/2020 - 11:01am

Here’s a fun little idea from Knut Synstad. You give it the URL of a GitHub Gist and it converts the Gist into grayscale rounded blobs (SVG) that sorta look like code if you squint. Maybe fun for interesting dynamic backgrounds or for whatever you might use code-looking stock art for.

It reminded me of Christian Naths’s Redacted font, which turns every glyph into a box or squiggles.

And if you need some actual totally fake code, Harry Parton’s Pen is nice for that:

CodePen Embed Fallback

The post Fake Code appeared first on CSS-Tricks.

Constrained CSS grids without `max-width`

Css Tricks - Tue, 04/21/2020 - 4:43am

Ain’t nothing wrong with max-width, but Ethan makes a point in the last sentence:

Rather than simply defaulting to max-width as a constraint, I can use the empty space around my design, and treat it as a layout tool.

If the space “around” your grid is actually part of the grid, it’s easier to use. Maybe you’d decide to scootch some author information up there after all, or show an ad, or who knows what. It will be more robust if you do it within the established grid.

Direct Link to ArticlePermalink

The post Constrained CSS grids without `max-width` appeared first on CSS-Tricks.

Drupal to Jamstack

Css Tricks - Tue, 04/21/2020 - 3:04am

I’ve been harping for a while that Jamstack doesn’t necessarily mean throwing away your old CMS. In fact, I’d argue that Jamstack is at it’s most powerful when paired with a system that you already know, are comfortable with, and perhaps even like. You’d call that decoupling the front end.

Netlify has a webinar coming up on exactly this, featuring Alex De Winne, who is going to show you a real-world site in a live demo combining Drupal (a classic PHP & MySQL CMS that powers an absolute ton of sites) and Jamstack architecture.

Register for Free Webinar

The post Drupal to Jamstack appeared first on CSS-Tricks.

Syndicate content
©2003 - Present Akamai Design & Development.