Hello, World!
- Create a new file
.env.local
and add environment variables with those values..env.local
carrot
banana
datePublisheddsadsa
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 = 1
As 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.local
and add environment variables with those values.
NEXT_PUBLIC_FIREBASE_PROJECT_ID=replace-me
FIREBASE_CLIENT_EMAIL=replace-me
FIREBASE_PRIVATE_KEY=replace-me
You 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_modules
anddist
folders (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.json
s 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 react
tsup
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 version
Components
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 here
Storybook
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/core
for 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>