Web Standards

“The Modern Web”

Css Tricks - Fri, 05/22/2020 - 10:11am

A couple of interesting articles making the rounds:

I like Tom’s assertion that React (which he’s using as a stand-in for JavaScript frameworks in general) has an ideal usage:

There is a sweet spot of React: in moderately interactive interfaces. Complex forms that require immediate feedback, UIs that need to move around and react instantly. That’s where it excels.

If there is anything I hope for the world of web design and development, it’s that we get better at picking the right tools for the job.

I heard several people hone in on this:

I can, for example, guarantee that this blog is faster than any Gatsby blog (and much love to the Gatsby team) because there is nothing that a React static site can do that will make it faster than a non-React static site.

One reaction was hell yes. React is a bunch of JavaScript and it does lots of stuff, but does not grant superpowers that make the web faster than it was without it. Another reaction was: well it actually does. That’s kind of the whole point of SPAs: not needing to reload the page. Instead, we’re able to make a trimmed network request for the new data needed for a new page and re-rendering only what is necessary.

Rich digs into that even more:

When I tap on a link on Tom’s JS-free website, the browser first waits to confirm that it was a tap and not a brush/swipe, then makes a request, and then we have to wait for the response. With a framework-authored site with client-side routing, we can start to do more interesting things. We can make informed guesses based on analytics about which things the user is likely to interact with and preload the logic and data for them. We can kick off requests as soon as the user first touches (or hovers) the link instead of waiting for confirmation of a tap — worst case scenario, we’ve loaded some stuff that will be useful later if they do tap on it. We can provide better visual feedback that loading is taking place and a transition is about to occur. And we don’t need to load the entire contents of the page — often, we can make do with a small bit of JSON because we already have the JavaScript for the page. This stuff gets fiendishly difficult to do by hand.

That’s what makes this stuff so easy to argue about. Everyone has good points. When we try to speak on behalf of the entire web, it’s tough for us all to agree. But the web is too big for broad, sweeping assertions.

Do people reach for React-powered SPAs too much? Probably, but that’s not without reason. There is innovation there that draws people in. The question is, how can we improve it?

From a front-of-the-front-end perspective, the fact that front-end frameworks like React encourage demand us write a front-end in components is compelling all by itself.

There is optimism and pessimism in both posts. The ending sentences of both are starkly different.

The post “The Modern Web” appeared first on CSS-Tricks.

The Fastest Google Fonts

Css Tricks - Fri, 05/22/2020 - 4:55am

When you use font-display: swap;, which Google Fonts does when you use the default &display=swap part of the URL , you’re already saying, “I’m cool with FOUT,” which is another way of saying web text is displayed right away, and when the web font is ready, “swap” to it.

There is already an async nature to what you are doing, so you might as well extend that async-ness to the rest of the font loading. Harry Roberts:

If you’re going to use font-display for your Google Fonts then it makes sense to asynchronously load the whole request chain.

Harry’s recommended snippet:

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preload" as="style" href="$CSS&display=swap" /> <link rel="stylesheet" href="$CSS&display=swap" media="print" onload="this.media='all'" />

$CSS is the main part of the URL that Google Fonts gives you.

Looks like a ~20% render time savings with no change in how it looks/feels when loading/. Other than that, it’s faster.

Direct Link to ArticlePermalink

The post The Fastest Google Fonts appeared first on CSS-Tricks.

A “new direction” in the struggle against rightward scrolling

Css Tricks - Thu, 05/21/2020 - 10:26am

You know those times you get a horizontal scrollbar when accidentally placing an element off the right edge of the browser window? It might be a menu that slides in or the like. Sometimes we to overflow-x: hidden; on the body to fix that, but that can sometimes wreck stuff like position: sticky;.

Well, you know how if you place an element off the left edge of a browser window, it doesn’t do that? That’s “data loss” and just how things work around here. It actually has to do with the direction of the page. If you were in a RTL situation, it would be the left edge of the browser window causing the overflow situation and the right edge where it doesn’t.

Emerson Loustau leverages that idea to solve a problem here. I’d be way too nervous messing with direction like this because I just don’t know what the side effects would be. But, hey, at least it doesn’t break position: sticky;.

Direct Link to ArticlePermalink

The post A “new direction” in the struggle against rightward scrolling appeared first on CSS-Tricks.

Flexbox-like “just put elements in a row” with CSS grid

Css Tricks - Thu, 05/21/2020 - 9:57am

It occurred to me while we were talking about flexbox and gap that one reason we sometimes reach for flexbox is to chuck some boxes in a row and space them out a little.

My brain still reaches for flexbox in that situation, and with gap, it probably will continue to do so. It’s worth noting though that grid can do the same thing in its own special way.

Like this:

.grid { display: grid; gap: 1rem; grid-auto-flow: column; } CodePen Embed Fallback

They all look equal width there, but that’s only because there is no content in them. With content, you’ll see the boxes start pushing on each other based on the natural width of that content. If you need to exert some control, you can always set width / min-width / max-width on the elements that fall into those columns — or, set them with grid-template-columns but without setting the actual number of columns, then letting the min-content dictate the width.

.grid { display: grid; gap: 1rem; grid-auto-flow: column; grid-template-columns: repeat(auto-fit, minmax(min-content, 1fr)); }

Flexible grids are the coolest.

Another thought… if you only want the whole grid itself to be as wide as the content (i.e. less than 100% or auto, if need be) then be aware that display: inline-grid; is a thing.

The post Flexbox-like “just put elements in a row” with CSS grid appeared first on CSS-Tricks.

How to Make Taxonomy Pages With Gatsby and Sanity.io

Css Tricks - Thu, 05/21/2020 - 4:58am

In this tutorial, we’ll cover how to make taxonomy pages with Gatsby with structured content from Sanity.io. You will learn how to use Gatsby’s Node creation APIs to add fields to your content types in Gatsby’s GraphQL API. Specifically, we’re going to create category pages for the Sanity’s blog starter.

That being said, there is nothing Sanity-specific about what we’re covering here. You’re able to do this regardless of which content source you may have. We’re just reaching for Sanity.io for the sake of demonstration.

Get up and running with the blog

If you want to follow this tutorial with your own Gatsby project, go ahead and skip to the section for creating a new page template in Gatsby. If not, head over to sanity.io/create and launch the Gatsby blog starter. It will put the code for Sanity Studio and the Gatsby front-end in your GitHub account and set up the deployment for both on Netlify. All the configuration, including example content, will be in place so that you can dive right into learning how to create taxonomy pages.

Once the project is iniated, make sure to clone the new repository on GitHub to local, and install the dependencies:

git clone git@github.com:username/your-repository-name.git cd your-repository-name npm i

If you want to run both Sanity Studio (the CMS) and the Gatsby front-end locally, you can do so by running the command npm run dev in a terminal from the project root. You can also cd into the web folder and just run Gatsby with the same command.

You should also install the Sanity CLI and log in to your account from the terminal: npm i -g @sanity/cli && sanity login. This will give you tooling and useful commands to interact with Sanity projects. You can add the --help flag to get more information on its functionality and commands.

We will be doing some customization to the gatsby-node.js file. To see the result of the changes, restart Gatsby’s development server. This is done in most systems by hitting CTRL + C in the terminal and running npm run dev again.

Getting familiar with the content model

Look into the /studio/schemas/documents folder. There are schema files for our main content types: author, category, site settings, and posts. Each of the files exports a JavaScript object that defines the fields and properties of these content types. Inside of post.js is the field definition for categories:

{ name: 'categories', type: 'array', title: 'Categories', of: [ { type: 'reference', to: [{ type: 'category' }] } ] },

This will create an array field with reference objects to category documents. Inside of the blog’s studio it will look like this:

An array field with references to category documents in the blog studio Adding slugs to the category type

Head over to /studio/schemas/documents/category.js. There is a simple content model for a category that consists of a title and a description. Now that we’re creating dedicated pages for categories, it would be handy to have a slug field as well. We can define that in the schema like this:

// studio/schemas/documents/category.js export default { name: 'category', type: 'document', title: 'Category', fields: [ { name: 'title', type: 'string', title: 'Title' }, { name: 'slug', type: 'slug', title: 'Slug', options: { // add a button to generate slug from the title field source: 'title' } }, { name: 'description', type: 'text', title: 'Description' } ] }

Now that we have changed the content model, we need to update the GraphQL schema definition as well. Do this by executing npm run graphql-deploy (alternatively: sanity graphql deploy) in the studio folder. You will get warnings about breaking changes, but since we are only adding a field, you can proceed without worry. If you want the field to accessible in your studio on Netlify, check the changes into git (with git add . && git commit -m"add slug field") and push it to your GitHub repository (git push origin master).

Now we should go through the categories and generate slugs for them. Remember to hit the publish button to make the changes accessible for Gatsby! And if you were running Gatsby’s development server, you’ll need to restart that too.

Quick sidenote on how the Sanity source plugin works

When starting Gatsby in development or building a website, the source plugin will first fetch the GraphQL Schema Definitions from Sanity deployed GraphQL API. The source plugin uses this to tell Gatsby which fields should be available to prevent it from breaking if the content for certain fields happens to disappear. Then it will hit the project’s export endpoint, which streams all the accessible documents to Gatsby’s in-memory datastore.

In order words, the whole site is built with two requests. Running the development server, will also set up a listener that pushes whatever changes come from Sanity to Gatsby in real-time, without doing additional API queries. If we give the source plugin a token with permission to read drafts, we’ll see the changes instantly. This can also be experienced with Gatsby Preview.

Adding a category page template in Gatsby

Now that we have the GraphQL schema definition and some content ready, we can dive into creating category page templates in Gatsby. We need to do two things:

  • Tell Gatsby to create pages for the category nodes (that is Gatsby’s term for “documents”).
  • Give Gatsby a template file to generate the HTML with the page data.

Begin by opening the /web/gatsby-node.js file. Code will already be here that can be used to create the blog post pages. We’ll largely leverage this exact code, but for categories. Let’s take it step-by-step:

Between the createBlogPostPages function and the line that starts with exports.createPages, we can add the following code. I’ve put in comments here to explain what’s going on:

// web/gatsby-node.js // ... async function createCategoryPages (graphql, actions) { // Get Gatsby‘s method for creating new pages const {createPage} = actions // Query Gatsby‘s GraphAPI for all the categories that come from Sanity // You can query this API on http://localhost:8000/___graphql const result = await graphql(`{ allSanityCategory { nodes { slug { current } id } } } `) // If there are any errors in the query, cancel the build and tell us if (result.errors) throw result.errors // Let‘s gracefully handle if allSanityCatgogy is null const categoryNodes = (result.data.allSanityCategory || {}).nodes || [] categoryNodes // Loop through the category nodes, but don't return anything .forEach((node) => { // Desctructure the id and slug fields for each category const {id, slug = {}} = node // If there isn't a slug, we want to do nothing if (!slug) return // Make the URL with the current slug const path = `/categories/${slug.current}` // Create the page using the URL path and the template file, and pass down the id // that we can use to query for the right category in the template file createPage({ path, component: require.resolve('./src/templates/category.js'), context: {id} }) }) }

Last, this function is needed at the bottom of the file:

// /web/gatsby-node.js // ... exports.createPages = async ({graphql, actions}) => { await createBlogPostPages(graphql, actions) await createCategoryPages(graphql, actions) // <= add the function here }

Now that we have the machinery to create the category page node in place, we need to add a template for how it actually should look in the browser. We’ll base it on the existing blog post template to get some consistent styling, but keep it fairly simple in the process.

// /web/src/templates/category.js import React from 'react' import {graphql} from 'gatsby' import Container from '../components/container' import GraphQLErrorList from '../components/graphql-error-list' import SEO from '../components/seo' import Layout from '../containers/layout' export const query = graphql` query CategoryTemplateQuery($id: String!) { category: sanityCategory(id: {eq: $id}) { title description } } ` const CategoryPostTemplate = props => { const {data = {}, errors} = props const {title, description} = data.category || {} return ( <Layout> <Container> {errors && <GraphQLErrorList errors={errors} />} {!data.category && <p>No category data</p>} <SEO title={title} description={description} /> <article> <h1>Category: {title}</h1> <p>{description}</p> </article> </Container> </Layout> ) } export default CategoryPostTemplate

We are using the ID that was passed into the context in gatsby-node.js to query the category content. Then we use it to query the title and description fields that are on the category type. Make sure to restart with npm run dev after saving these changes, and head over to localhost:8000/categories/structured-content in the browser. The page should look something like this:

A barebones category page

Cool stuff! But it would be even cooler if we actually could see what posts that belong to this category, because, well, that’s kinda the point of having categories in the first place, right? Ideally, we should be able to query for a “pages” field on the category object.

Before we learn how to that, we need to take a step back to understand how Sanity’s references work.

Querying Sanity’s references

Even though we’re only defining the references in one type, Sanity’s datastore will index them “bi-directionally.” That means creating a reference to the “Structured content” category document from a post lets Sanity know that the category has these incoming references and will keep you from deleting it as long as the reference exists (references can be set as “weak” to override this behavior). If we use GROQ, we can query categories and join posts that have them like this (see the query and result in action on groq.dev):

*[_type == "category"]{ _id, _type, title, "posts": *[_type == "post" && references(^._id)]{ title, slug } } // alternative: *[_type == "post" && ^._id in categories[]._ref]{

This ouputs a data structure that lets us make a simple category post template:

[ { "_id": "39d2ca7f-4862-4ab2-b902-0bf10f1d4c34", "_type": "category", "title": "Structured content", "posts": [ { "title": "Exploration powered by structured content", "slug": { "_type": "slug", "current": "exploration-powered-by-structured-content" } }, { "title": "My brand new blog powered by Sanity.io", "slug": { "_type": "slug", "current": "my-brand-new-blog-powered-by-sanity-io" } } ] }, // ... more entries ] That’s fine for GROQ, what about GraphQL?

Here‘s the kicker: As of yet, this kind of query isn’t possible with Gatsby’s GraphQL API out of the box. But fear not! Gatsby has a powerful API for changing its GraphQL schema that lets us add fields.

Using createResolvers to edit Gatsby’s GraphQL API

Gatsby holds all the content in memory when it builds your site and exposes some APIs that let us tap into how it processes this information. Among these are the Node APIs. It’s probably good to clarify that when we are talking about “node” in Gatsby — not to be confused with Node.js. The creators of Gatsby have borrowed “edges and nodes” from Graph theory where “edges” are the connections between the “nodes” which are the “points” where the actual content is located. Since an edge is a connection between nodes, it can have a “next” and “previous” property.

The edges with next and previous, and the node with fields in GraphQL’s API explorer

The Node APIs are used by plugins first and foremost, but they can be used to customize how our GraphQL API should work as well. One of these APIs is called createResolvers. It’s fairly new and it lets us tap into how a type’s nodes are created so we can make queries that add data to them.

Let’s use it to add the following logic:

  • Check for ones with the SanityCategory type when creating the nodes.
  • If a node matches this type, create a new field called posts and set it to the SanityPost type.
  • Then run a query that filters all posts that has lists a category that matches the current category’s ID.
  • If there are matching IDs, add the content of the post nodes to this field.

Add the following code to the /web/gatsby-node.js file, either below or above the code that’s already in there:

// /web/gatsby-node.js // Notice the capitalized type names exports.createResolvers = ({createResolvers}) => { const resolvers = { SanityCategory: { posts: { type: ['SanityPost'], resolve (source, args, context, info) { return context.nodeModel.runQuery({ type: 'SanityPost', query: { filter: { categories: { elemMatch: { _id: { eq: source._id } } } } } }) } } } } createResolvers(resolvers) }

Now, let’s restart Gatsby’s development server. We should be able to find a new field for posts inside of the sanityCategory and allSanityCategory types.

Adding the list of posts to the category template

Now that we have the data we need, we can return to our category page template (/web/src/templates/category.js) and add a list with links to the posts belonging to the category.

// /web/src/templates/category.js import React from 'react' import {graphql, Link} from 'gatsby' import Container from '../components/container' import GraphQLErrorList from '../components/graphql-error-list' import SEO from '../components/seo' import Layout from '../containers/layout' // Import a function to build the blog URL import {getBlogUrl} from '../lib/helpers' // Add “posts” to the GraphQL query export const query = graphql` query CategoryTemplateQuery($id: String!) { category: sanityCategory(id: {eq: $id}) { title description posts { _id title publishedAt slug { current } } } } ` const CategoryPostTemplate = props => { const {data = {}, errors} = props // Destructure the new posts property from props const {title, description, posts} = data.category || {} return ( <Layout> <Container> {errors && <GraphQLErrorList errors={errors} />} {!data.category && <p>No category data</p>} <SEO title={title} description={description} /> <article> <h1>Category: {title}</h1> <p>{description}</p> {/* If there are any posts, add the heading, with the list of links to the posts */} {posts && ( <React.Fragment> <h2>Posts</h2> <ul> { posts.map(post => ( <li key={post._id}> <Link to={getBlogUrl(post.publishedAt, post.slug)}>{post.title}</Link> </li>)) } </ul> </React.Fragment>) } </article> </Container> </Layout> ) } export default CategoryPostTemplate

This code will produce this simple category page with a list of linked posts – just liked we wanted!

Go make taxonomy pages!

We just completed the process of creating new page types with custom page templates in Gatsby. We covered one of Gatsby’s Node APIs called createResolver and used it to add a new posts field to the category nodes.

This should give you what you need to make other types of taxonomy pages! Do you have multiple authors on your blog? Well, you can use the same logic to create author pages. The interesting thing with the GraphQL filter is that you can use it to go beyond the explicit relationship made with references. It can also be used to match other fields using regular expressions or string comparisons. It’s fairly flexible!

The post How to Make Taxonomy Pages With Gatsby and Sanity.io appeared first on CSS-Tricks.

Roll Your Own Comments With Gatsby and FaunaDB

Css Tricks - Thu, 05/21/2020 - 4:57am

If you haven’t used Gatsby before have a read about why it’s fast in every way that matters, and if you haven’t used FaunaDB before you’re in for a treat. If you’re looking to make your static sites full blown Jamstack applications this is the back end solution for you!

This tutorial will only focus on the operations you need to use FaunaDB to power a comment system for a Gatsby blog. The app comes complete with inputs fields that allow users to comment on your posts and an admin area for you to approve or delete comments before they appear on each post. Authentication is provided by Netlify’s Identity widget and it’s all sewn together using Netlify serverless functions and an Apollo/GraphQL API that pushes data up to a FaunaDB database collection.

I chose FaunaDB for the database for a number of reasons. Firstly there’s a very generous free tier! perfect for those small projects that need a back end, there’s native support for GraphQL queries and it has some really powerful indexing features!

…and to quote the creators;

No matter which stack you use, or where you’re deploying your app, FaunaDB gives you effortless, low-latency and reliable access to your data via APIs familiar to you

You can see the finished comments app here.

Get Started

To get started clone the repo at https://github.com/PaulieScanlon/fauna-gatsby-comments

or:

git clone https://github.com/PaulieScanlon/fauna-gatsby-comments.git

Then install all the dependencies:

npm install

Also cd in to functions/apollo-graphql and install the dependencies for the Netlify function:

npm install

This is a separate package and has its own dependencies, you’ll be using this later.

We also need to install the Netlify CLI as you’ll also use this later:

npm install netlify-cli -g

Now lets add three new files that aren’t part of the repo.

At the root of your project create a .env .env.development and .env.production

Add the following to .env:

GATSBY_FAUNA_DB = GATSBY_FAUNA_COLLECTION =

Add the following to .env.development:

GATSBY_FAUNA_DB = GATSBY_FAUNA_COLLECTION = GATSBY_SHOW_SIGN_UP = true GATSBY_ADMIN_ID =

Add the following to .env.production:

GATSBY_FAUNA_DB = GATSBY_FAUNA_COLLECTION = GATSBY_SHOW_SIGN_UP = false GATSBY_ADMIN_ID =

You’ll come back to these later but in case you’re wondering

  • GATSBY_FAUNA_DB is the FaunaDB secret key for your database
  • GATSBY_FAUNA_COLLECTION is the FaunaDB collection name
  • GATSBY_SHOW_SIGN_UP is used to hide the Sign up button when the site is in production
  • GATSBY_ADMIN_ID is a user id that Netlify Identity will generate for you

If you’re the curious type you can get a taster of the app by running gatsby develop or yarn develop and then navigate to http://localhost:8000 in your browser.

FaunaDB

So Let’s get cracking, but before we write any operations head over to https://fauna.com/ and sign up!

Database and Collection
  • Create a new database by clicking NEW DATABASE
  • Name the database: I’ve called the demo database fauna-gatsby-comments
  • Create a new Collection by clicking NEW COLLECTION
  • Name the collection: I’ve called the demo collection demo-blog-comments
Server Key

Now you’ll need to to set up a server key. Go to SECURITY

  • Create a new key by clicking NEW KEY
  • Select the database you want the key to apply to, fauna-gatsby-comments for example
  • Set the Role as Admin
  • Name the server key: I’ve called the demo key demo-blog-server-key
Environment Variables Pt. 1

Copy the server key and add it to GATSBY_FAUNA_DB in .env.development, .env.production and .env.

You’ll also need to add the name of the collection to GATSBY_FAUNA_COLLECTION in .env.development, .env.production and .env.

Adding these values to .env are just so you can test your development FaunaDB operations, which you’ll do next.

Let’s start by creating a comment so head back to boop.js:

// boop.js ... // CREATE COMMENT createComment: async () => { const slug = "/posts/some-post" const name = "some name" const comment = "some comment" const results = await client.query( q.Create(q.Collection(COLLECTION_NAME), { data: { isApproved: false, slug: slug, date: new Date().toString(), name: name, comment: comment, }, }) ) console.log(JSON.stringify(results, null, 2)) return { commentId: results.ref.id, } }, ...

The breakdown of this function is as follows;

  • q is the instance of faunadb.query
  • Create is the FaunaDB method to create an entry within a collection
  • Collection is area in the database to store the data. It takes the name of the collection as the first argument and a data object as the second.

The second argument is the shape of the data you need to drive the applications comment system.

For now you’re going to hard-code slug, name and comment but in the final app these values are captured by the input form on the posts page and passed in via args

The breakdown for the shape is as follows;

  • isApproved is the status of the comment and by default it’s false until we approve it in the Admin page
  • slug is the path to the post where the comment was written
  • date is the time stamp the comment was written
  • name is the name the user entered in the comments from
  • comment is the comment the user entered in the comments form

When you (or a user) creates a comment you’re not really interested in dealing with the response because as far as the user is concerned all they’ll see is either a success or error message.

After a user has posted a comment it will go in to your Admin queue until you approve it but if you did want to return something you could surface this in the UI by returning something from the createComment function.

Create a comment

If you’ve hard coded a slug, name and comment you can now run the following in your CLI

node boop createComment

If everything worked correctly you should see a log in your terminal of the new comment.

{ "ref": { "@ref": { "id": "263413122555970050", "collection": { "@ref": { "id": "demo-blog-comments", "collection": { "@ref": { "id": "collections" } } } } } }, "ts": 1587469179600000, "data": { "isApproved": false, "slug": "/posts/some-post", "date": "Tue Apr 21 2020 12:39:39 GMT+0100 (British Summer Time)", "name": "some name", "comment": "some comment" } } { commentId: '263413122555970050' }

If you head over to COLLECTIONS in FaunaDB you should see your new entry in the collection.

You’ll need to create a few more comments while in development so change the hard-coded values for name and comment and run the following again.

node boop createComment

Do this a few times so you end up with at least three new comments stored in the database, you’ll use these in a moment.

Delete comment by id

Now that you can create comments you’ll also need to be able to delete a comment.

By adding the commentId of one of the comments you created above you can delete it from the database. The commentId is the id in the ref.@ref object

Again you’re not really concerned with the return value here but if you wanted to surface this in the UI you could do so by returning something from the deleteCommentById function.

// boop.js ... // DELETE COMMENT deleteCommentById: async () => { const commentId = "263413122555970050"; const results = await client.query( q.Delete(q.Ref(q.Collection(COLLECTION_NAME), commentId)) ); console.log(JSON.stringify(results, null, 2)); return { commentId: results.ref.id, }; }, ...

The breakdown of this function is as follows

  • client is the FaunaDB client instance
  • query is a method to get data from FaunaDB
  • q is the instance of faunadb.query
  • Delete is the FaunaDB delete method to delete entries from a collection
  • Ref is the unique FaunaDB ref used to identify the entry
  • Collection is area in the database where the data is stored

If you’ve hard coded a commentId you can now run the following in your CLI:

node boop deleteCommentById

If you head back over to COLLECTIONS in FaunaDB you should see that entry no longer exists in collection

Indexes

Next you’re going to create an INDEX in FaunaDB.

An INDEX allows you to query the database with a specific term and define a specific data shape to return.

When working with GraphQL and / or TypeScript this is really powerful because you can use FaunaDB indexes to return only the data you need and in a predictable shape. This makes data typing responses in GraphQL and / TypeScript a dream… I’ve worked on a number of applications that just return a massive object of useless values which will inevitably cause bugs in your app. blurg!

  • Go to INDEXES and click NEW INDEX
  • Name the index: I’ve called this one get-all-comments
  • Set the source collection to the name of the collection you setup earlier

As mentioned above when you query the database using this index you can tell FaunaDB which parts of the entry you want to return.

You can do this by adding “values” but be careful to enter the values exactly as they appear below because (on the FaunaDB free tier) you can’t amend these after you’ve created them so if there’s a mistake you’ll have to delete the index and start again… bummer!

The values you need to add are as follows:

  • ref
  • data.isApproved
  • data.slug
  • data.date
  • data.name
  • data.comment

After you’ve added all the values you can click SAVE.

Get all comments // boop.js ... // GET ALL COMMENTS getAllComments: async () => { const results = await client.query( q.Paginate(q.Match(q.Index("get-all-comments"))) ); console.log(JSON.stringify(results, null, 2)); return results.data.map(([ref, isApproved, slug, date, name, comment]) => ({ commentId: ref.id, isApproved, slug, date, name, comment, })); }, ...

The breakdown of this function is as follows

  • client is the FaunaDB client instance
  • query is a method to get data from FaunaDB
  • q is the instance of faunadb.query
  • Paginate paginates the responses
  • Match returns matched results
  • Index is the name of the Index you just created

The shape of the returned result here is an array of the same shape you defined in the Index “values”

If you run the following you should see the list of all the comments you created earlier:

node boop getAllComments Get comments by slug

You’re going to take a similar approach as above but this time create a new Index that allows you to query FaunaDB in a different way. The key difference here is that when you get-comments-by-slug you’ll need to tell FaunaDB about this specific term and you can do this by adding data.slug to the Terms field.

  • Go to INDEX and click NEW INDEX
  • Name the index, I’ve called this one get-comments-by-slug
  • Set the source collection to the name of the collection you setup earlier
  • Add data.slug in the terms field

The values you need to add are as follows:

  • ref
  • data.isApproved
  • data.slug
  • data.date
  • data.name
  • data.comment

After you’ve added all the values you can click SAVE.

// boop.js ... // GET COMMENT BY SLUG getCommentsBySlug: async () => { const slug = "/posts/some-post"; const results = await client.query( q.Paginate(q.Match(q.Index("get-comments-by-slug"), slug)) ); console.log(JSON.stringify(results, null, 2)); return results.data.map(([ref, isApproved, slug, date, name, comment]) => ({ commentId: ref.id, isApproved, slug, date, name, comment, })); }, ...

The breakdown of this function is as follows:

  • client is the FaunaDB client instance
  • query is a method to get data from FaunaDB
  • q is the instance of faunadb.query
  • Paginate paginates the responses
  • Match returns matched results
  • Index is the name of the Index you just created

The shape of the returned result here is an array of the same shape you defined in the Index “values” you can create this shape in the same way you did above and be sure to add a value for terms. Again be careful to enter these with care.

If you run the following you should see the list of all the comments you created earlier but for a specific slug:

node boop getCommentsBySlug Approve comment by id

When you create a comment you manually set the isApproved value to false. This prevents the comment from being shown in the app until you approve it.

You’ll now need to create a function to do this but you’ll need to hard-code a commentId. Use a commentId from one of the comments you created earlier:

// boop.js ... // APPROVE COMMENT BY ID approveCommentById: async () => { const commentId = '263413122555970050' const results = await client.query( q.Update(q.Ref(q.Collection(COLLECTION_NAME), commentId), { data: { isApproved: true, }, }) ); console.log(JSON.stringify(results, null, 2)); return { isApproved: results.isApproved, }; }, ...

The breakdown of this function is as follows:

  • client is the FaunaDB client instance
  • query is a method to get data from FaunaDB
  • q is the instance of faunadb.query
  • Update is the FaundaDB method up update an entry
  • Ref is the unique FaunaDB ref used to identify the entry
  • Collection is area in the database where the data is stored

If you’ve hard coded a commentId you can now run the following in your CLI:

node boop approveCommentById

If you run the getCommentsBySlug again you should now see the isApproved status of the entry you hard-coded the commentId for will have changed to true.

node boop getCommentsBySlug

These are all the operations required to manage the data from the app.

In the repo if you have a look at apollo-graphql.js which can be found in functions/apollo-graphql you’ll see the all of the above operations. As mentioned before the hard-coded values are replaced by args, these are the values passed in from various parts of the app.

Netlify

Assuming you’ve completed the Netlify sign up process or already have an account with Netlify you can now push the demo app to your GitHub account.

To do this you’ll need to have initialize git locally, added a remote and have pushed the demo repo upstream before proceeding.

You should now be able to link the repo up to Netlify’s Continuous Deployment.

If you click the “New site from Git” button on the Netlify dashboard you can authorize access to your GitHub account and select the gatsby-fauna-comments repo to enable Netlify’s Continuous Deployment. You’ll need to have deployed at least once so that we have a pubic URL of your app.

The URL will look something like this https://ecstatic-lewin-b1bd17.netlify.app but feel free to rename it and make a note of the URL as you’ll need it for the Netlify Identity step mentioned shortly.

Environment Variables Pt. 2

In a previous step you added the FaunaDB database secret key and collection name to your .env files(s). You’ll also need to add the same to Netlify’s Environment variables.

  • Navigate to Settings from the Netlify navigation
  • Click on Build and deploy
  • Either select Environment or scroll down until you see Environment variables
  • Click on Edit variables

Proceed to add the following:

GATSBY_SHOW_SIGN_UP = false GATSBY_FAUNA_DB = you FaunaDB secret key GATSBY_FAUNA_COLLECTION = you FaunaDB collection name

While you’re here you’ll also need to amend the Sensitive variable policy, select Deploy without restrictions

Netlify Identity Widget

I mentioned before that when a comment is created the isApproved value is set to false, this prevents comments from appearing on blog posts until you (the admin) have approved them. In order to become admin you’ll need to create an identity.

You can achieve this by using the Netlify Identity Widget.

If you’ve completed the Continuous Deployment step above you can navigate to the Identity page from the Netlify navigation.

You wont see any users in here just yet so lets use the app to connect the dots, but before you do that make sure you click Enable Identity

Before you continue I just want to point out you’ll be using netlify dev instead of gatsby develop or yarn develop from now on. This is because you’ll be using some “special” Netlify methods in the app and staring the server using netlify dev is required to spin up various processes you’ll be using.

  • Spin up the app using netlify dev
  • Navigate to http://localhost:8888/admin/
  • Click the Sign Up button in the header

You will also need to point the Netlify Identity widget at your newly deployed app URL. This was the URL I mentioned you’ll need to make a note of earlier, if you’ve not renamed your app it’ll look something like this https://ecstatic-lewin-b1bd17.netlify.app/ There will be a prompt in the pop up window to Set site’s URL.

You can now complete the necessary sign up steps.

After sign up you’ll get an email asking you to confirm you identity and once that’s completed refresh the Identity page in Netlify and you should see yourself as a user.

It’s now login time, but before you do this find Identity.js in src/components and temporarily un-comment the console.log() on line 14. This will log the Netlify Identity user object to the console.

  • Restart your local server
  • Spin up the app again using netlify dev
  • Click the Login button in the header

If this all works you should be able to see a console log for netlifyIdentity.currentUser: find the id key and copy the value.

Set this as the value for GATSBY_ADMIN_ID = in both .env.production and .env.development

You can now safely remove the console.log() on line 14 in Identity.js or just comment it out again.

GATSBY_ADMIN_ID = your Netlify Identity user id

…and finally

  • Restart your local server
  • Spin up the app again using netlify dev

Now you should be able to login as “Admin”… hooray!

Navigate to http://localhost:8888/admin/ and Login.

It’s important to note here you’ll be using localhost:8888 for development now and NOT localhost:8000 which is more common with Gatsby development

Before you test this in the deployed environment make sure you go back to Netlify’s Environment variables and add your Netlify Identity user id to the Environment variables!

  • Navigate to Settings from the Netlify navigation
  • Click on Build and deploy
  • Either select Environment or scroll down until you see Environment variables
  • Click on Edit variables

Proceed to add the following:

GATSBY_ADMIN_ID = your Netlify Identity user id

If you have a play around with the app and enter a few comments on each of the posts then navigate back to Admin page you can choose to either approve or delete the comments.

Naturally only approved comments will be displayed on any given post and deleted ones are gone forever.

If you’ve used this tutorial for your project I’d love to hear from you at @pauliescanlon.

By Paulie Scanlon (@pauliescanlon), Front End React UI Developer / UX Engineer: After all is said and done, structure + order = fun.

Visit Paulie’s Blog at: www.paulie.dev

The post Roll Your Own Comments With Gatsby and FaunaDB appeared first on CSS-Tricks.

Radio Buttons Are Like Selects; Checkboxes Are Like Multiple Selects

Css Tricks - Wed, 05/20/2020 - 4:41am

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

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

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

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

CodePen Embed Fallback

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

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

WordPress Block Transforms

Css Tricks - Tue, 05/19/2020 - 1:52pm

This has been the year of Gutenberg for us here at CSS-Tricks. In fact, that’s a goal we set at the end of last year. We’re much further along that I thought we’d be, authoring all new content in the block editor¹, enabling the block editor for all content now. That means when we open most old posts, we see all the content in the “Classic” block. It looks like this:

A post written on CSS-Tricks before we were using the block editor.

The entire contents of the post is in a single block, so not exactly making any use of the block editor. It’s still “visual,” like the block editor, but it’s more like the old visual editor using TinyMCE. I never used that as it kinda forcefully mangled HTML in a way I didn’t like.

This is the #1 thing I was worried about

Transforming a Classic block into new blocks is as trivial as selecting the Classic block and selecting the “Convert to Blocks” option.

Select the option and the one block becomes many blocks.

How does the block editor handle block-izing old content, when we tell it to do that from the “Convert to Blocks” option? What if it totally screws up content during the conversion? Will we ever be able to switch?

The answer: it does a pretty darn good job. But… there are still issues. Not “bugs” but situations where we have custom HTML in our old content and it doesn’t know what to do with it — let alone how to convert it into exactly the blocks we wish it would. There is a way!

Basic Block Transforms

That’s where this idea of “Block Transforms” comes in. All (well, most?) native blocks have “to” and “from” transformations. You’re probably already familiar with how it manifests in the UI. Like a paragraph can transform “to” a quote and vice versa. Here’s a super meta screenshot of this very paragraph:

Those transforms aren’t magic; they are very explicitly coded. When you register a block, you specify the transforms. Say you were registering your own custom code block. You’d want to make sure that you could transform it…

  • From and to the default built-in code block, and probably a handful of others that might be useful.
  • Back to the built-in code block.

Which might look like:

registerBlockType("my/code-block", { title: __("My Code Block"), ... transforms: { from: [ { type: "block", priority: 7, blocks: ["core/code", "core/paragraph", "core/preformatted"], transform: function (attributes) { return createBlock("my/code-block", { content: attributes.content, }); }, }, ], to: [ { type: "block", blocks: ["core/code"], transform: ({ content }) => createBlock("core/code", { content }), }, ], ...

Those are transforms to and from other blocks. Fortunately, this is a pretty simple block where we’re just shuffling the content around. More complex blocks might need to pass around more data, but I haven’t had to deal with that yet.

The more magical stuff: Block Transforms from raw code

Here’s the moment of truth for old content:

The “Convert to Blocks” option.

In this situation, blocks are being created not from other blocks, but from raw code. Quite literally, the HTML is being looked at and choices are being made about what blocks to make from chunks of that HTML. This is where it’s amazing the block editor does such a good job with the choices, and also where things can go wrong and it can fail, make wrong block choices, or mangle content.

In our old content, a block of code (a super very important thing) in a post would look like this:

<pre rel="JavaScript"><code class="language-javascript" markup="tt"> let html = `<div>cool</div>`; </code></pre>

Sometimes the block conversion would do OK on those, turning it into a native code block. But there were a number of problems:

  1. I don’t want a native code block. I want that to be transformed into our own new code block (blogged about that here).
  2. I need some of the information in those attributes to inform settings on the new block, like what kind of code it is.
  3. The HTML in our old code blocks was not escaped and I need it to not choke on that.

I don’t have all the answers here, as this is an evolving process, but I do have some block transforms in place now that are working pretty well. Here’s what a “raw” transform (as opposed to a “block” transform) looks like:

registerBlockType("my/code-block", { title: __("My Code Block"), // ... transforms: { from: [ { type: "block", priority: 7, // ... }, { type: "raw", priority: 8, isMatch: (node) => node.nodeName === "PRE" && node.children.length === 1 && node.firstChild.nodeName === "CODE", transform: function (node) { let pre = node; let code = node.querySelector("code"); let codeType = "html"; if (pre.classList.contains("language-css")) { codeType = "css"; } if (pre.getAttribute("rel") === "CSS") { codeType = "css"; } if (pre.classList.contains("language-javascript")) { codeType = "javascript"; } if (code.classList.contains("language-javascript")) { codeType = "javascript"; } // ... other data wrangling... return createBlock("csstricks/code-block", { content: code.innerHTML, codeType: codeType, }); }, }, ], to: [ // ... ], // ... }

That isMatch function runs on every node in the HTML it finds, so this is the big opportunity to return true from that in the special situations you need to. Note in the code above that I’m specifically looking for HTML that looks like <pre ...><code ...>. When that matches, the transform runs, and I can return a createBlock call that passes in data and content I extract from the node with JavaScript.

Another example: Pasting a URL

“Raw” transforms don’t only happen when you “Convert to Blocks.” They happen when you paste content into the block editor too. You’ve probably experienced this before. Say you have copied some table markup from somewhere and paste it into the block editor -— it will probably paste as a table. A YouTube URL might paste into an embed. This kind of thing is why copy/pasting from Word documents and the like tend to work so well with the block editor.

Say you want some special behavior when a certain type of URL is pasted into the editor. This was the situation I was in with our custom CodePen Embed block. I wanted it so if you pasted a codepen.io URL, it would use this custom block, instead of the default embed.

This is a “from” transform that looks like this:

{ type: "raw", priority: 8, // higher number to beat out default isMatch: (node) => node.nodeName === "P" && node.innerText.startsWith("https://codepen.io/"), transform: function (node) { return createBlock("cp/codepen-gutenberg-embed-block", { penURL: node.innerText, penID: getPenID(node.innerText), // helper function }); }, } So…

Is it messy? A little. But it’s as powerful as you need it to be. If you have an old site with lots of bespoke HTML and shortcodes and stuff, then getting into block transforms is the only ticket out.

I’m glad I went to WPBlockTalk and caught K. Adam White’s talk on shortcodes because there was just one slide that clued me into that this was even possible. There is a little bit of documentation on it.

One thing I’d like to figure out is if it’s possible to run these transforms on all old content in the database. Seems a little scary, but also like it might be a good idea in some situations. Once I get my transformations really solid, I could see doing that so any old content ready-to-go in the block editor when opening it up. I just have no idea how to go about it.

I’m glad to be somewhat on top of this though, as I friggin love the block editor right now. It’s a pleasure to write in and build content with it. I like what Justin Tadlock said:

The block system is not going anywhere. WordPress has moved beyond the point where we should consider the block editor as a separate entity. It is an integral part of WordPress and will eventually touch more and more areas outside of the editing screen.

It’s here to stay. Embracing the block editor and bending it to our will is key.

  1. What are we calling it anyway? “Gutenberg” doesn’t seem right anymore. Feels like that will fade away, even though the development of it still happens in the Gutenberg plugin. I think I’ll just call it “the block editor” unless specifically referring to that plugin.

The post WordPress Block Transforms appeared first on CSS-Tricks.

How to Build a Chrome Extension

Css Tricks - Tue, 05/19/2020 - 4:38am

I made a Chrome extension this weekend because I found I was doing the same task over and over and wanted to automate it. Plus, I’m a nerd living through a pandemic, so I spend my pent-up energy building things. I’ve made a few Chrome Extensions over the years, hope this post helps you get going, too. Let’s get started!

Create the manifest

The first step is creating a manifest.json file in a project folder. This serves a similar purpose to a package.json, it provides the Chrome Web Store with critical information about the project, including the name, version, the required permissions, and so forth. Here’s an example:

{ "manifest_version": 2, "name": "Sample Name", "version": "1.0.0", "description": "This is a sample description", "short_name": "Short Sample Name", "permissions": ["activeTab", "declarativeContent", "storage", "<all_urls>"], "content_scripts": [ { "matches": ["<all_urls>"], "css": ["background.css"], "js": ["background.js"] } ], "browser_action": { "default_title": "Does a thing when you do a thing", "default_popup": "popup.html", "default_icon": { "16": "icons/icon16.png", "32": "icons/icon32.png" } } }

You might notice a few things- first: the names and descriptions can be anything you’d like.

The permissions depend on what the extension needs to do. We have ["activeTab", "declarativeContent", "storage", "<all_urls>"] in this example because this particular extension needs information about the active tab, needs to change the page content, needs to access localStorage, and needs to be active on all sites. If it only needs it to be active on one site at a time, we can remove the last index of that array. 

A list of all of the permissions and what they mean can be found in Chrome’s extension docs.

"content_scripts": [ { "matches": ["<all_urls>"], "css": ["background.css"], "js": ["background.js"] } ],

The content_scripts section sets the sites where the extension should be active. If you want a single site, like Twitter for example, you would say ["https://twitter.com/*"]. The CSS and JavaScript files are everything needed for extensions. For instance, my productive Twitter extension uses these files to override Twitter’s default appearance.

"browser_action": { "default_title": "Does a thing when you do a thing", "default_popup": "popup.html", "default_icon": { "16": "icons/icon16.png", "32": "icons/icon32.png" } }

There are things in browser_action that are also optional. For example, if the extension doesn’t need a popup for its functionality, then both the default_title and default_popup can be removed. In that case, all that’s needed the icon for the extension. If the extension only works on some sites, then Chrome will grey out the icon when it’s inactive.

Debugging

Once the manifest, CSS and JavaScript files are ready, head over to chrome://extensions/from the browser’s address bar and enable developer mode. That activates the “Load unpacked” button to add the extension files. It’s also possible to toggle whether or not the developer version of the extension is active.

I would highly recommend starting a GitHub repository to version control the files at this point. It’s a good way to save the work.

The extension needs to be reloaded from this interface when it is updated. A little refresh icon will display on the screen. Also, if the extension has any errors during development, it will show an error button with a stack trace and more info here as well.

Popup functionality

If the extension need to make use of a popup that comes off the extension icon, it’s thankfully fairly straightforward. After designating the name of the file with browser_action in the manifest file, a page can be built with whatever HTML and CSS you’ll like to include, including images (I tend to use inline SVG).

We’ll probably want to add some functionality to a popup. That make take some JavaScript, so make sure the JavaScript file is designated in the manifest file and is linked up in your popup file as well, like this: <script src="background.js"></script>

In that file, start by creating functionality and we’ll have access to the popup DOM like this:

document.addEventListener("DOMContentLoaded", () => { var button = document.getElementById("submit") button.addEventListener("click", (e) => { console.log(e) }) })

If we create a button in the popup.html file, assign it an ID called submit, and then return a console log, you might notice that nothing is actually logged in the console. That’s because we’re in a different context, meaning we’ll need to right-click on the popup and open up a different set of DevTools.

We now have access to logging and debugging! Keep in mind, though, that if anything is set in localStorage, then it will only exist in the extension’s DevTools localStorage; not the user’s browser localStorage. (This bit me the first time I tried it!)

Running scripts outside the extension

This is all fine and good, but say we want to run a script that has access to information on the current tab? Here’s a couple of ways we would do this. I would typically call a separate function from inside the DOMContentLoaded event listener:

Example 1: Activate a file function exampleFunction() { chrome.tabs.executeScript(() => { chrome.tabs.executeScript({ file: "content.js" }) }) } Example 2: Execute just a bit of code

This way is great if there’s only a small bit of code to run. However, it quickly gets tough to work with since it requires passing everything as a string or template literal.

function exampleFunction() { chrome.tabs.executeScript({ code: `console.log(‘hi there’)` }) } Example 3: Activate a file and pass a parameter

Remember, the extension and tab are operating in different contexts. That makes passing parameters between them a not-so-trivial task. What we’ll do here is nest the first two examples to pass a bit of code into the second file. I will store everything I need in a single option, but we’ll have to stringify the object for that to work properly.

function exampleFunction(options) { chrome.tabs.executeScript( { code: "var options = " + JSON.stringify(options) }, function() { chrome.tabs.executeScript({ file: "content.js" }) } ) } Icons

Even though the manifest file only defines two icons, we need two more to officially submit the extension to the Chrome Web Store: one that’s 128px square, and one that I call icon128_proper.png, which is also 128px, but has a little padding inside it between the edge of the image and the icon.

Keep in mind that whatever icon is used needs to look good both in light mode and dark mode for the browser. I usually find my icons on the Noun Project.

Submitting to the Chrome Web Store

Now we get to head over to the Chrome Web Store developer console to submit the extension! Click the “New Item” button, the drag and drop the zipped project file into the uploader.

From there, Chrome will ask a few questions about the extension, request information about the permissions requested in the extension and why they’re needed. Fair warning: requesting “activeTab” or “tabs” permissions will require a longer review to make sure the code isn’t doing anything abusive.

That’s it! This should get you all set up and on your way to building a Chrome browser extension! &#x1f389;

The post How to Build a Chrome Extension appeared first on CSS-Tricks.

Using BugHerd to Track Visual Feedback on Websites

Css Tricks - Tue, 05/19/2020 - 3:32am

BugHerd is about collecting visual feedback for websites.

If you’re like me, you’re constantly looking at your own websites and you’re constantly critiquing them. I think that’s healthy. Nothing gets better if you look at your own work and consider it perfectly finished. This is where BugHerd shines. With BugHerd, anytime you have one of those little “uh oh this area is a little rough” moments while looking at your site, you can log it to be dealt with.

Let’s take a look at a workflow like that. I’m going to assume you’ve signed up for a BugHerd account (if not grab a free trial here) and either installed the script on your site or have installed the browser extension and are using that.

I’ve done that for this very site. So now I’m looking at a page like our Archives Page, and I spot some stuff that is a little off.

I’ve taken a screenshot and circled the things that I think are visually off:

  1. The “Top Tags” and dropdown arrow are pretty far separated with nothing much connecting them. Maybe dropdowns like that should have a background or border to make that more clear.
  2. There is a weird shadow in the middle of the bottom line.

With BugHerd, I can act upon that stuff immediately. Rather than some janky workflow involving manual screenshots and opening tickets on some other unrelated website, I can do it right from the site itself.

  1. I open the BugHerd sidebar
  2. I click the green + button
  3. Select the element around where I want to give the visual feedback
  4. Enter the details of the bug

Their help video does a great job of showing this.

Here’s me logging one of those bugs I found:

Now, the BugHerd website becomes my dashboard for dealing with visual bugs. This unlocks a continual cycle of polish that that is how great websites get great!

Note the kanban board setup, which is always my prefered way to work on any project. Cards are things that need to be worked on and there are columns for cards that aren’t started, started, and finished. Perhaps your team works another way though? Maybe you have a few more columns you generally kanban with, or you name them in a specific way? That’s totally customizable in BugHerd.

I love that BugHerd itself is customizable, but at a higher level, the entire workflow is customizable and that’s even better.

  • I can set up BugHerd just for myself and use it for visual improvement work on my own projects
  • I can set up BugHerd for just the design team and we can use it among ourselves to track visual issues and get them fixed.
  • I can set up BugHerd for the entire company, so everyone feels empowered to call out visual rough spots.
  • I can set up BugHerd for clients, if I’m a freelancer or agency worker, so that the clients themselves can use it to report visual feedback.
  • I can open up BugHerd wide open so that guests of these websites can use it to report visual problems.

Check out this example of a design team with core members and guests and their preferred workflow setup:

It’s hard to imagine a better dedicated tool than BugHerd for visual feedback.

The post Using BugHerd to Track Visual Feedback on Websites appeared first on CSS-Tricks.

First Steps into a Possible CSS Masonry Layout

Css Tricks - Mon, 05/18/2020 - 10:58am

It’s not at the level of demand as, say, container queries, but being able to make “masonry” layouts in CSS has been a big ask for CSS developers for a long time. Masonry being that kind of layout where unevenly-sized elements are layed out in ragged rows. Sorta like a typical brick wall turned sideways.

The layout alone is achievable in CSS alone already, but with one big caveat: the items aren’t arranged in rows, they are arranged in columns, which is often a deal-breaker for folks.

/* People usually don't want this */ 1 4 6 8 2 7 3 5 9 /* They want this *. 1 2 3 4 5 6 7 8 9

If you want that ragged row thing and horizontal source order, you’re in JavaScript territory. Until now, that is, as Firefox rolled this out under a feature flag in Firefox Nightly, as part of CSS grid.

Mats Palmgren:

An implementation of this proposal is now available in Firefox Nightly. It is disabled by default, so you need to load about:config and set the preference layout.css.grid-template-masonry-value.enabled to true to enable it (type “masonry” in the search box on that page and it will show you that pref).

Jen Simmons has created some demos already:

CodePen Embed Fallback Is this really a grid?

A bit of pushback from Rachel Andrew

Grid isn’t Masonry, because it’s a grid with strict rows and columns. If you take another look at the layout created by Masonry, we don’t have strict rows and columns. Typically we have defined rows, but the columns act more like a flex layout, or Multicol. The key difference between the layout you get with Multicol and a Masonry layout, is that in Multicol the items are displayed by column. Typically in a Masonry layout you want them displayed row-wise.

[…]

Speaking personally, I am not a huge fan of this being part of the Grid specification. It is certainly compelling at first glance, however I feel that this is a relatively specialist layout mode and actually isn’t a grid at all. It is more akin to flex layout than grid layout.

By placing this layout method into the Grid spec I worry that we then tie ourselves to needing to support the Masonry functionality with any other additions to Grid.

None of this is final yet, and there is active CSS Working Group discussion about it.

As Jen said:

This is an experimental implementation — being discussed as a possible CSS specification. It is NOT yet official, and likely will change. Do not write blog posts saying this is definitely a thing. It’s not a thing. Not yet. It’s an experiment. A prototype. If you have thoughts, chime in at the CSSWG.

Houdini?

Last time there was chatter about native masonry, it was mingled with idea that the CSS Layout API, as part of Houdini, could do this. That is a thing, as you can see by opening this demo (repo) in Chrome Canary.

I’m not totally up to speed on whether Houdini is intended to be a thing so that ideas like this can be prototyped in the browser and ultimately moved out of Houdini, or if the ideas should just stay in Houdini, or what.

The post First Steps into a Possible CSS Masonry Layout appeared first on CSS-Tricks.

Unprefixed `appearance `

Css Tricks - Mon, 05/18/2020 - 10:56am

It’s interesting how third-parties are sometimes super involved in pushing browser things forward. One big story there was how Bloomberg hired Igalia to implement CSS grid across the browsers.

Here’s another story of Bocoup doing that, this time for the appearance property. The story is told in a Twitter thread, but the thread is broken somehow (looks like a deleted Tweet), so your best bet is to go to this one, then scroll up and down to see the whole thing. Gosh, I hope they blog it.

It took literally years of work:

2 years ago, @firefox asked us to work on a project to fix problems within the CSS appearance property. The issue came when we found out that each browser has its own implementation of how the appearance property should work on forms.

They had to do tons of research, write tests, and ultimately overhaul HTML and CSS specs. Then they needed to prove that, with those changes, browsers could un-prefix the property without breaking websites — the first attempt at this broke websites and was reverted. Then they actually get all three major browsers to do it. (Landed in Chrome, Firefox is on it, Safari has an open bug, and there is public desire to coordinate a release.)

Really goes to show just how long and grueling this work can be because it’s so crucial to get it right. If you’re into this stuff, listen to ShopTalk 407 with Brian Kardell.

The post Unprefixed `appearance ` appeared first on CSS-Tricks.

CSS fix for 100vh in mobile WebKit

Css Tricks - Fri, 05/15/2020 - 11:45am

A surprisingly common response when asking people about things they’d fix about anything in CSS, is to improve the handling of viewport units.

One thing that comes up often is how they relate to scrollbars. For example, if an element is sized to 100vw and stretches edge-to-edge, that’s fine so long as the page doesn’t have a vertical scrollbar. If it does have a vertical scrollbar, then 100vw is too wide, and the presence of that vertical scrollbar triggers a horizontal scrollbar because viewport units don’t have an elegant/optional way of handling that. So you might be hiding overflow on the body when you otherwise wouldn’t need to, for example. (Demo)

Another scenario involves mobile browsers. You might use viewport units to help you position a fixed footer along the bottom of the screen. But then browser chrome might come up (e.g. navigation, keyboard, etc), and it may cover the footer, because the mobile browser doesn’t consider anything changed about the viewport size.

Matt Smith documents this problem:

On the left, the browser navigation bar (considered browser chrome) is covering up the footer making it appear that the footer is beyond 100vh when it is not. On the right, the -webkit-fill-available property is being used rather than viewport units to fix the problem.

And a solution of sorts:

body { min-height: 100vh; min-height: -webkit-fill-available; } html { height: -webkit-fill-available; }

The above was updated to make sure the html element was being used, as we were told Chrome is updating the behavior to match Firefox’s implementation.

Does this really work? […] I’ve had no problems with any of the tests I’ve run and I’m using this method in production right now. But I did receive a number of responses to my tweet pointing to other possible problems with using this (the effects of rotating devices, Chrome not completely ignoring the property, etc.)

It would be better to get some real cross-browser solution for this someday, but I don’t see any issues using this as an improvement. It’s weird to use a vendor-prefixed property as a progressive enhancement, but hey, the world is weird.

Direct Link to ArticlePermalink

The post CSS fix for 100vh in mobile WebKit appeared first on CSS-Tricks.

Comparing Social Media Outlets for Developer Tips

Css Tricks - Fri, 05/15/2020 - 11:34am

As a little experiment, I shared a development tip on three different social networks. I also tried to post it in a format that was most suitable for that particular social network:

How did each of them “do”? Let’s take a look. But bear in mind… this ain’t scientific. This is just me having a glance at one isolated example to get a feel for things across different social media sites.

The Twitter Thread The Tweet

A little journey with lists, as a &#x1f9f5; thread.

`list-style-position: outside;` is the default for lists, and is a pretty decent default. The best part about it is that both the markers *and* the content are aligned. pic.twitter.com/CkQv1hIt6q

— CSS-Tricks (@css) April 27, 2020

Twitter is probably our largest social media outlet. Despite the fact that I’ve done absolutely nothing with it this year other than auto-tweeting posts from this site (via our Jetpack Integration), those tweets do just about as well as it ever did when I was writing each tweet. These numbers are bound to change, but at the time of writing:

Views

102,501

Followers

~446,000

Retweets

108

Engagements

3,753

Likes

428 (first tweet)

Twitter provides analytics on tweets

Going off that engagements number, a little bit less than 1% of the followers had anything to do with it. I’d say this was a very average tweet for us, if not on the low side.

The Instagram Post The Post View this post on Instagram

There are alignment things to consider with lists like an <ol>. The markers and the content. Outside positioning does well. But it uses the edge of the box as alignment and renders markers outside the box which can be bad for getting cut off. There is a solution with custom counters and subgrid though!

A post shared by Chris Coyier (@real_css_tricks) on Apr 26, 2020 at 5:44pm PDT

Instagram is by far the smallest of our social media outlets, being newer and not something I stay particularly active or consistent on. No auto-posting there just yet.

Followers

~2,800

Likes

308

Reached

2,685

Instagram provides analytics (“insights”) on posts.

Using Reach, that’s 96% of the followers. That’s pretty incredible compared t 1% of followers on Twitter. Although, on Twitter. I can easily put URLs to tweets and send people places, where my only options on Instagram are “check out the link in my profile” or use a swipe-up thing in an Instagram Story. So, despite the high engagement of Instagram, I’m mostly just getting the satisfaction of teaching something as well as a little brand awareness. It’s much harder for me to get you to directly do something from Instagram.

The YouTube Video The Video

YouTube is in the middle for us, much bigger than Instagram but not as big as Twitter. YouTube is a little unique in that there can be (and are) advertising directly on the videos and that get’s a “revenue share” from YouTube. That’s very much not driving motivation for using YouTube (I make 50 cents a day, but it is unique compared to the others.

Subscribers

51,300

Likes

116

Views

2,455

YouTube provides video analytics Facebook?

We do have a Facebook page but it’s the most neglected of all of them. We auto-post new articles to it, but this experiment didn’t really have a blog post. I published the video to our site, but that doesn’t get auto-posted to Facebook, so the tip never made it there.

I used to feel a little guilty about not taking as much advantage of Facebook as I could, but whenever I look at overall analytics, I’m reminded that all of social media accounts combine for ~2% of traffic to this site. Spending any more time on this stuff is foolish for me, when that time could be spent on content for this site and information architecture for what we already have. And for Facebook specifically, whatever time we have spent there has never seemed to pan out. Just not a hive for developers.

CodePen?

I probably should have factored CodePen into this more, since it’s something of a social network itself with similar metrics. I worked on the examples in CodePen and the whole video was done in CodePen. But in this case, it was more about the journey than the destination. I did ultimately link to a demo at the end of the Twitter thread, but Instagram can’t link to it and I wasn’t as compelled to link to it on YouTube as the video itself to me was the important information.

If I was trying to compare CodePen stats here, I would have created the Pen in a step-by-step educational format so I could deliver the same idea. That actually sounds fun and I should probably still do that!

Winner?

Eh.

The problem is that there isn’t anything particularly useful to measure. What would have been way more interesting is if I had some really important call to action in each one where I’m like trying to sell you something or get you to sign up for something or whatever. I feel like that’s the real world of developer marketing. You gotta do 100 things for someone for free if you want them to do something for you on that 101st time. And on the 101st time, you should probably measure it somehow to see if the effort is worth it.

Here’s the very basic data together though…

FollowersEngagements%Twitter~446,0003,7530.08%Instagram~2,8002,68596%YouTube~51,3002,4555%

One interesting thing is that I find the effort was about equal for all of them. You’d think a video would be hardest, but at least that’s just hit-record-hit-stop and minor editing. The other formats take longer to craft with custom text and graphics.

These would be my takeaways from this limited experiment:

  • You need big numbers on Twitter to do much. That’s because the engagement is pretty low. Still, it’s probably our best outlet for getting people to click a link and do something.
  • Instagram has amazing engagement, but it’s hard to send anyone anywhere. It’s still no wonder why people use it. You really do reach your audience there. If you had a strong call to action, I bet you could still get people do to it even with the absence of links (since people know how to search for stuff on the web).
  • While I mentioned that for this example the effort level was fairly even, in general, YouTube is going to require much higher effort. Video production just isn’t the same as farting out a couple of words or a screenshot. With that, and knowing that you’d need absolutely massive numbers to earn anything directly from YouTube, it’s pretty similar to other social networks in that you need to derive value from it abstractly.
  • This was not an idea that “went viral” in any sense. This is just standard-grade engagement, which was good for this experiment. I’m always super surprised at the type of developer tips that go viral. It’s always something I don’t expect, and often something I’m like awwwww we have an article about that too! I’d never bet on or expect anything going viral. Making stuff that your normal audience likes is the ticket.
  • Being active is pretty important. Any chart I’ve seen has big peaks when posts go out regularly and valleys when they don’t. Post regularly = riding the peaks.
  • None of this compares anywhere close to the real jewel of making things: blogging. Blogging is where you have full control and full benefit. The most important thing social media can do is get people over to your own site.

The post Comparing Social Media Outlets for Developer Tips appeared first on CSS-Tricks.

WTF is a Static API

Css Tricks - Fri, 05/15/2020 - 4:40am

Just like there is a movement to make more websites (and more of websites) from pre-rendered static files (Jamstack), so to might we consider moving content-based APIs to be static. Sean C Davis:

static API is simply a collection of flat JSON files that live on a content delivery network (CDN). It doesn’t perform any action other than delivering content (static JSON files) to the requesting user.

But that doesn’t mean a static API is simple. And every file doesn’t have to be manually generated or updated. A static API can still use a database and it can still pull data from external services. In other words, it can be dynamically generated, but it is statically delivered.

The use cases are probably somewhat limited, but I still like it. In a sense, all RSS feeds (or, more directly, JSON feeds) could be done this way, and many are.

Direct Link to ArticlePermalink

The post WTF is a Static API appeared first on CSS-Tricks.

Using UX testing to dramatically improve your A/B testing results

Usability Geek - Thu, 05/14/2020 - 10:50am
We are used to thinking of A/B split testing as a way to improve UX and increase conversions. But it’s actually a two-way road.  How you design your tests should be based on UX research –...
Categories: Web Standards

Notion-Powered Websites

Css Tricks - Thu, 05/14/2020 - 10:10am

I’m a big fan of Notion, as you likely know from previous coverage and recent video. It’s always interesting to see what other people do with Notion, and even how Notion uses Notion.

I’d say most usage of Notion is private and internal, but any page on Notion can be totally public with the flip of a switch. We do that with some stuff like our post ideas page and here’s a simple camping checklist I made for myself.

That’s pretty rad. You could use that for lots of business-things you might otherwise literally build a website to do. Maybe a public product roadmap, a job posting, a press release an announcement…

But it’s not quite a website. You don’t get a custom domain name. You don’t have any analytics on it. You’re limited by the exact feature set of Notion.

People have been trying to extract the data out of Notion and essentially use it as a CMS for a while now…

But all those ways are, ya know, a decent amount of effort.

Stephen Ou recently showed me a pretty cool idea he has called Fruition. It’s entirely free, and also a bit of effort to set up, but once you’re done, you have your own domain name that hosts a Notion page and gives you some control over it (like putting in fonts and scripts and such).

It’s very clever in that it uses Cloudflare Workers to do all the heavy lifting.

This is probably the easiest-to-manage website ever. Just open Notion, change stuff, done.

Stephen admits Fruition is somewhat complex to set up. If you’re looking for something easier and perhaps more flexible, check out Super.

Actual neumorphism in the wild!

I would note that none of these things are official Notion products or affiliates of it in any way. Honestly, they all make me a little nervous in that they could break if Notion ever decides they don’t care for the product to be used this way. I also feel like Notion has been saying an API is something they’d like to offer for a while. That will be the real answer to all this and there will be a proliferation of third-party products once we have it.

The post Notion-Powered Websites appeared first on CSS-Tricks.

Online Together

Css Tricks - Thu, 05/14/2020 - 4:16am

(This is a sponsored post.)

An Event Apart: Online Together is a single-day online conference with an intense focus on digital design, UX, content, code, and more, giving you deep insights into where we are now and where things are going next.

AEA! With a brand new online version of their conference! That’s awesome. AEA is a best-in-class web conference, so if you’re looking for a conference to help get your mind going on big-picture web design and development stuff again and connect with some like-minded industry people, this is your opportunity.

Direct Link to ArticlePermalink

The post Online Together appeared first on CSS-Tricks.

Equal Width Columns in CSS Grid are Kinda Weird

Css Tricks - Wed, 05/13/2020 - 1:13pm

Everything is flexible these days. If you write grid-template-columns: 200px 200px 200px;, sure, you’d have equal-width columns, but that’s a rare day. What you usually mean is three columns of equal fluid width.

We’ve got fractional units for that, like grid-template-columns: 1fr 1fr fr;. That’s usually fine, but they aren’t very sturdy like pixels. A large bit of media (or something like a <pre>, or long bit of text like a URL) can cause those columns to stretch and that’s almost never what you want. I’ve called that a grid blowout. The big idea is that the minimum width of a 1fr column is auto, not 0. In other words,. those widened columns are just being as narrow as they know how to be!

To fix that, we can do like:

.el { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); }

..or we could shorten it:

.el { grid-template-columns: repeat(3, minmax(0, 1fr)); }

It’s a little awkward, but OK. You only gotta learn it once. You might not even ever run into this if you’re always setting max-width on your media and handling line breaks.

If you want to make your columns sturdy again without the minmax dance, you could use percentages rather than pixels and stay flexible. But what percentage do you use? 33.33%? That’s fine as long as you don’t have any gap — otherwise, the gap will add to the width and overflow the container. You could fake gaps by putting padding inside the columns, but that’s a little janky and uneven.

This whole thing comes from a great tweet from Wes Bos:

&#x1f525; A visual guide to getting equal width columns in CSS Grid pic.twitter.com/PY4yYokY18

— Wes Bos (@wesbos) May 1, 2020

I know a ton of people run into this — based on the number of emails I get about the grid blowout article — so it’s worth kinda internalizing why all this is the way it is. It probably should be easier but I don’t have any particular suggestions on how it could be.

The post Equal Width Columns in CSS Grid are Kinda Weird appeared first on CSS-Tricks.

Pseudo-elements in the Web Animations API

Css Tricks - Wed, 05/13/2020 - 1:13pm

To use the Web Animations API (e.g. el.animate()) you need a reference to a DOM element to target. So, how do you use it on pseudo-elements, which don’t really offer a direct reference? Dan Wilson covers a (newish?) part of the API itself:

const logo = document.getElementById('logo'); logo.animate({ opacity: [0, 1] }, { duration: 100, pseudoElement: '::after' });

I noticed in Dan’s article that ::marker is supported. I was just playing with that recently while doing our List Style Recipes page. I figured I’d give it a spin by testing the WAAPI and @keyframes on both a ::marker and and ::after element:

CodePen Embed Fallback

At first, I confused myself because it seemed like the WAAPI wasn’t working on ::after, but Dan reminded me that when using a transform, the element can’t be display: inline. Instead, I made it inline-block and it worked fine. However, I did uncover that @keyframes don’t seem to work on ::marker elements in Firefox — hopefully they’ll fix that (and we get Chrome and Safari support for ::marker ASAP).

Direct Link to ArticlePermalink

The post Pseudo-elements in the Web Animations API appeared first on CSS-Tricks.

Syndicate content
©2003 - Present Akamai Design & Development.