Hello, World!
- Create a new file
.env.localand add environment variables with those values..env.local
carrot
bananadatePublisheddsadsa
const [age, setAge] = useState(50)
const [name, setName] = useState('Taylor')const something = 1
const something = 1
const something = 1
const something = 1
const something = 1
const something = 1
const something = 1
const something = 1
const something = 1As you walk through the office at work reading the news on your phone, you enter an elevator. You had just attempted to load a new page only to be greeted with a painful loading spinner. No one likes this experience.
It's inevitable that some users of your application will have slow connections. A well thought out design accounts for varying internet speeds and displays a loading state to the user. However, making the user stare at a spinning wheel for an extended period of time can drastically increase bounce rates. What if there was a better way?
Skeleton Screens
Skeleton screens build anticipation for the content that is going to appear whereas loading spinners (and progress bars) put the focus on the wait time that the user has to endure. Apple has agreed with this idea enough to incorporate skeleton screens into their iOS Human Interface Guidelines. They recommend displaying an outline of the initial application without text or any elements that will change. This can improve the feel of any action taking longer than a few hundred milliseconds.
Examples
By now, you've probably seen some examples of skeleton screens in your daily browsing without even noticing. For example - Facebook shows users gray circles and lines to represent the contents of a post in their timeline.
It's not just Facebook either. LinkedIn has also re-designed their layout to use placeholders.
You can trick your users into thinking your website loads faster using skeleton screens. Let's look at how you can actually create this effect using some simple HTML and Scss.
Building a Placeholder
First, let's create the base structure. In this example, the placeholder is supposed to represent a text area. We'll use BEM (Base - Element - Modifier) naming for our classes.
- Create a project in Firebase.
- In the Firebase console, open Settings > Service Accounts.
- Click Generate New Private Key, then confirm by clicking Generate Key.
- Download and open the JSON file containing your service account.
- Create a new file
.env.localand add environment variables with those values.
NEXT_PUBLIC_FIREBASE_PROJECT_ID=replace-me
FIREBASE_CLIENT_EMAIL=replace-me
FIREBASE_PRIVATE_KEY=replace-meYou can now fetch data from Firebase directly inside a Server Component in the app directory:
import 'server-only'
import { notFound } from 'next/navigation'
import * as admin from 'firebase-admin'
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
}),
})
}
const db = admin.firestore()
export default async function Page() {
const user = await db.collection('users').doc('leerob').get()
if (!user.exists) {
notFound()
}
return <div>Hello, {user.data().name}!</div>
}<div class="text-input__loading">
<div class="text-input__loading--line"></div>
<div class="text-input__loading--line"></div>
<div class="text-input__loading--line"></div>
<div class="text-input__loading--line"></div>
<div class="text-input__loading--line"></div>
<div class="text-input__loading--line"></div>
<div class="text-input__loading--line"></div>
<div class="text-input__loading--line"></div>
<div class="text-input__loading--line"></div>
<div class="text-input__loading--line"></div>
</div>Each line should be about the same height as our text. We can use CSS animation to create a pulsating effect.
&--line {
height: 10px;
margin: 10px;
animation: pulse 1s infinite ease-in-out;
}Next, let's define how our pulse animation should work. We can modify the opacity of the background color using rgba to provide an opacity between 0.0 and 1.0.
@keyframes pulse {
0% {
background-color: rgba(165, 165, 165, 0.1);
}
50% {
background-color: rgba(165, 165, 165, 0.3);
}
100% {
background-color: rgba(165, 165, 165, 0.1);
}
}We also want to vary the width of each loading line. Let's create a Sass mixin to apply the given content to each nth-child in a list.
@mixin nth-children($points...) {
@each $point in $points {
&:nth-child(#{$point}) {
@content;
}
}
}We can use the newly created mixin to change the width of all 10 children div elements.
@include nth-children(1, 5, 9) {
width: 150px;
}
@include nth-children(2, 6, 10) {
width: 250px;
}
@include nth-children(3, 7) {
width: 50px;
}
@include nth-children(4, 8) {
width: 100px;
}Final Result š

You can view the code and a live example on CodePen. There's also a React library called react-placeholder that achieves the same effect.
Further Reading:
-
š Turborepo ā High-performance build system for Monorepos
-
š React ā JavaScript library for user interfaces
-
š Tsup ā TypeScript bundler powered by esbuild
-
š Storybook ā UI component environment powered by Vite
As well as a few others tools preconfigured:
- TypeScript for static type checking
- ESLint for code linting
- Prettier for code formatting
- Changesets for managing versioning and changelogs
- GitHub Actions for fully automated package publishing
Getting Started
Clone the design system example locally or from GitHub:
npx degit vercel/turborepo/examples/design-system design-system
cd design-system
yarn install
git init . && git add . && git commit -m "Init"Useful Commands
yarn build- Build all packages including the Storybook siteyarn dev- Run all packages locally and preview with Storybookyarn lint- Lint all packagesyarn changeset- Generate a changesetyarn clean- Clean up allnode_modulesanddistfolders (runs each package's clean script)
Turborepo
Turborepo is a high-performance build system for JavaScript and TypeScript codebases. It was designed after the workflows used by massive software engineering organizations to ship code at scale. Turborepo abstracts the complex configuration needed for monorepos and provides fast, incremental builds with zero-configuration remote caching.
Using Turborepo simplifes managing your design system monorepo, as you can have a single lint, build, test, and release process for all packages. Learn more about how monorepos improve your development workflow.
Apps & Packages
This Turborepo includes the following packages and applications:
apps/docs: Component documentation site with Storybookpackages/@acme/core: Core React componentspackages/@acme/utils: Shared React utilitiespackages/@acme/tsconfig: Sharedtsconfig.jsons used throughout the Turborepopackages/eslint-preset-acme: ESLint preset
Each package and app is 100% TypeScript. Yarn Workspaces enables us to "hoist" dependencies that are shared between packages to the root package.json. This means smaller node_modules folders and a better local dev experience. To install a dependency for the entire monorepo, use the -W workspaces flag with yarn add.
This example sets up your .gitignore to exclude all generated files, other folders like node_modules used to store your dependencies.
Compilation
To make the core library code work across all browsers, we need to compile the raw TypeScript and React code to plain JavaScript. We can accomplish this with tsup, which uses esbuild to greatly improve performance.
Running yarn build from the root of the Turborepo will run the build command defined in each package's package.json file. Turborepo runs each build in parallel and caches & hashes the output to speed up future builds.
For acme-core, the build command is the following:
tsup src/index.tsx --format esm,cjs --dts --external reacttsup compiles src/index.tsx, which exports all of the components in the design system, into both ES Modules and CommonJS formats as well as their TypeScript types. The package.json for acme-core then instructs the consumer to select the correct format:
{
"name": "@acme/core",
"version": "0.0.0",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"sideEffects": false
}Run yarn build to confirm compilation is working correctly. You should see a folder acme-core/dist which contains the compiled output.
acme-core
āāā dist
āāā index.t.ts <-- Types
āāā index.js <-- CommonJS version
āāā index.mjs <-- ES Modules versionComponents
Each file inside of acme-core/src is a component inside our design system. For example:
import * as React from 'react'
export interface ButtonProps {
children: React.ReactNode
}
export function Button(props: ButtonProps) {
return <button>{props.children}</button>
}
Button.displayName = 'Button'When adding a new file, ensure the component is also exported from the entry index.tsx file:
import * as React from 'react'
export { Button, type ButtonProps } from './Button'
// Add new component exports hereStorybook
Storybook provides us with an interactive UI playground for our components. This allows us to preview our components in the browser and instantly see changes when developing locally. This example preconfigures Storybook to:
- Use Vite to bundle stories instantly (in milliseconds)
- Automatically find any stories inside the
stories/folder - Support using module path aliases like
@acme/corefor imports - Write MDX for component documentation pages
For example, here's the included Story for our Button component:
import { Button } from '@acme/core/src';
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
<Meta title="Components/Button" component={Button} />
# Button
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec euismod, nisl eget consectetur tempor, nisl nunc egestas nisi, euismod aliquam nisl nunc euismod.
## Props
<Props of={Box} />
## Examples
<Preview>
<Story name="Default">
<Button>Hello</Button>
</Story>
</Preview>