Skip to content

Snippets.

React components, hooks, helpers and the odd shell life hack, plus the docs and answers I keep going back to. Components come with a preview; everything else is code you can take straight out.

42 snippets · 8 bookmarks

Snippets.

42
01Component

Button

Two variants, pill geometry, and a className that can still override the base. No variant library needed.

Save changesCancelDisabled
import { cn } from '@/lib/cn'

type Props = React.ComponentProps<'button'> & {
  variant?: 'solid' | 'ghost'
}

export function Button({ variant = 'solid', className, ...rest }: Props) {
  return (
    <button
      className={cn(
        'inline-flex items-center gap-2 rounded-full',
        'px-5 py-3 text-sm font-medium transition-colors',
        'disabled:pointer-events-none disabled:opacity-50',
        variant === 'solid' && 'bg-stone-900 text-stone-50',
        variant === 'ghost' && 'border border-stone-300',
        className,
      )}
      {...rest}
    />
  )
}
ui/button.tsx
04Utility

cn(): class merge

Conditional classes that also resolve Tailwind conflicts, so a prop can override a base class instead of fighting it.

import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
lib/cn.ts
05Hook · Motion

useReveal: enter on scroll

One observer, unobserved after the first hit. This is the rise-and-fade used across this site.

import { useEffect, useRef, useState } from 'react'

export function useReveal<T extends HTMLElement>() {
  const ref = useRef<T>(null)
  const [shown, setShown] = useState(false)

  useEffect(() => {
    const el = ref.current
    if (!el) return
    const io = new IntersectionObserver(([e]) => {
      if (!e.isIntersecting) return
      setShown(true)
      io.unobserve(el)
    }, { rootMargin: '-12% 0px' })
    io.observe(el)
    return () => io.disconnect()
  }, [])

  return { ref, shown }
}
use-reveal.ts
08Hook

useDebouncedValue

Debounce the value, not the handler. Feed it straight into a query key so typing does not fire a request per keystroke.

import { useEffect, useState } from 'react'

export function useDebouncedValue<T>(value: T, ms = 300) {
  const [debounced, setDebounced] = useState(value)

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), ms)
    return () => clearTimeout(id)
  }, [value, ms])

  return debounced
}

// const q = useDebouncedValue(input, 250)
// useQuery({ queryKey: ['search', q], ... })
use-debounced-value.ts
09Component

Skeleton

Match the shape of the content it replaces, not a generic grey box, and let it hold still for anyone who asked for less motion.

import { cn } from '@/lib/cn'

export function Skeleton({ className }: { className?: string }) {
  return (
    <div
      aria-hidden
      className={cn(
        'animate-pulse rounded bg-stone-200',
        'motion-reduce:animate-none',
        className,
      )}
    />
  )
}

// <Skeleton className="h-3 w-4/5" />
ui/skeleton.tsx
12CSS

Three lines of type polish

Balanced headlines, no orphan lines in body copy, and motion that switches itself off for people who asked.

h1, h2, h3 { text-wrap: balance; }
p, li      { text-wrap: pretty; }

/* never ship an animation without this */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
globals.css
13Next.js

Images from an external host

next/image refuses hosts it has not been told about. domains is the old way; remotePatterns is the one to reach for now.

// the old way: a flat list of hostnames
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  images: {
    domains: ["ecoplease-edit.site"],
  },
};

module.exports = nextConfig;

// https://nextjs.org/docs/messages/next-image-unconfigured-host
const config = {
  images: {
    remotePatterns: [
      {
        hostname: "jalan-karbon-bucket.s3.ap-southeast-1.amazonaws.com",
      },
    ],
  },
};
next.config.js
16Layout · Tailwind

Grid rows that fit their content

Same idea as the flex version, for grids: auto rows let each row size itself to its tallest cell.

<div className="grid grid-cols-2 grid-rows-[auto]">
  <div className="h-[auto/fit]" />
</div>
layout.tsx
18CSS

One-liners worth remembering

Figures that stop jittering, headlines without widows, text that inverts over anything, a height that respects mobile browser chrome, and long words that wrap.

.numbers   { font-variant: tabular-nums; }
.title     { text-wrap: balance; }
.overlay   { mix-blend-mode: difference; }
.container { height: 100svh; }

p { word-break: break-word; }
globals.css
19Utility

formatTime: seconds to mm:ss

For countdowns and media players. padStart keeps the width fixed so the digits do not shuffle as they tick.

export default function formatTime(time: number): string {
  const minutes = Math.floor(time / 60);
  const seconds = time % 60;

  return `${minutes.toString().padStart(2, "0")}:${seconds
    .toString()
    .padStart(2, "0")}`;
}
lib/format-time.ts
22React · TypeScript

Generic component through forwardRef

forwardRef swallows generics. Cast the result back to a generic signature so the options prop keeps its type at the call site.

import { type ClassValue } from "clsx";
import { forwardRef, type ReactNode, type Ref } from "react";

type MainLayoutProps<T extends SelectOption> = React.HTMLAttributes<HTMLDivElement> & {
  backgroundColor?: ClassValue;
  isFullWidth?: boolean;
  isAddTopPadding?: boolean;
  isDisableMaxWidth?: boolean;
  isAddVerticalPadding?: boolean;
  isOverflowHidden?: boolean;
  options?: T;
};

const MainLayoutComponent = forwardRef(
  <T extends SelectOption>(
    { ...props }: MainLayoutProps<T>,
    ref: Ref<HTMLDivElement>,
  ) => {
    return (
      <div ref={ref} {...props}>
        {props.children}
      </div>
    );
  },
);

MainLayoutComponent.displayName = "MainLayout";

export const MainLayout = MainLayoutComponent as <T extends SelectOption>(
  props: MainLayoutProps<T> & { ref?: Ref<HTMLDivElement> },
) => ReactNode;
main-layout.tsx
24Next.js · Node

Custom Next.js server

When a route has to be rendered by hand before Next handles the rest. Middleware needs hostname and port passed in here.

import { createServer } from "http";
import { parse } from "url";
import next from "next";

const dev = process.env.NODE_ENV !== "production";
const hostname = "localhost";
const port = 8080;
// when using middleware `hostname` and `port` must be provided below
const app = next({ dev, hostname, port });
const handle = app.getRequestHandler();

void app.prepare().then(() => {
  createServer((req, res) => {
    try {
      // Be sure to pass `true` as the second argument to `url.parse`.
      // This tells it to parse the query portion of the URL.
      const parsedUrl = parse(req.url ?? "", true);
      const { pathname, query } = parsedUrl;

      if (pathname === "/a") {
        void app.render(req, res, "/a", query);
      } else if (pathname === "/b") {
        void app.render(req, res, "/b", query);
      } else {
        void handle(req, res, parsedUrl);
      }
    } catch (err) {
      console.error("Error occurred handling", req.url, err);
      res.statusCode = 500;
      res.end("internal server error");
    }
  }).listen(port, (err) => {
    if (err) throw err;
    console.log(`> Ready on http://${hostname}:${port}`);
  });
});
server.js
26React · Hook Form

Keep your own ref with register()

register() owns the ref. Pull it out, call it yourself, then assign to your own ref too. Both keep working.

import React, { useRef } from "react";
import { useForm } from "react-hook-form";

type Inputs = {
  firstName: string,
  lastName: string,
};

export default function App() {
  const { register, handleSubmit } = useForm<Inputs>();
  const firstNameRef = useRef<HTMLInputElement | null>(null);
  const onSubmit = data => console.log(data);
  const { ref, ...rest } = register("firstName");

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...rest} name="firstName" ref={(e) => {
        ref(e)
        firstNameRef.current = e // you can still assign to ref
      }} />

      <button>Submit</button>
    </form>
  );
}
app.tsx
28TypeScript

Literal union that still autocompletes

Adding | string collapses the union to plain string and kills autocomplete. string & {} accepts any string but keeps the suggestions.

type Color = "primary" | "secondary" | string;
// hover shows: type Color = string

// ------

type Color = "primary" | "secondary" | (string & {});
// hover shows: type Color = "primary" | "secondary" | (string & {})
types.ts
30Next.js · Server Action

revalidatePath from the client

A one-line server action so a client component can bust a cached route after a mutation.

"use server";

import { revalidatePath } from "next/cache";

export async function revalidate(path: string) {
  return revalidatePath(path);
}
actions/revalidate.ts
32Hook · Scroll

Scroll direction for a hiding navbar

requestAnimationFrame-throttled, with a threshold so small jitters do not flip the nav back and forth.

const [isScrollUp, setIsScrollUp] = useState(true);

useEffect(() => {
  const threshold = 50;
  let lastScrollY = window.scrollY;
  let ticking = false;

  const updateScrollDir = () => {
    const scrollY = window.scrollY;

    if (Math.abs(scrollY - lastScrollY) < threshold) {
      ticking = false;
      return;
    }
    if (!isOpen) setIsScrollUp(scrollY < lastScrollY);
    lastScrollY = scrollY > 0 ? scrollY : 0;
    ticking = false;
  };
  const onScroll = () => {
    if (!ticking) {
      window.requestAnimationFrame(updateScrollDir);
      ticking = true;
    }
  };
  window.addEventListener("scroll", onScroll);

  return () => window.removeEventListener("scroll", onScroll);
}, [isOpen]);
use-scroll-direction.ts
34Layout · Tailwind

truncate inside a grid

truncate does nothing in a column that is allowed to grow. grid-cols-1 caps it; grid-cols-[auto_1fr] lets the title size itself instead.

// title truncates to the column width
<div className="grid flex-1 grid-cols-1 gap-1">
  <Paragraph className="truncate font-medium">{title}</Paragraph>
  <Paragraph className="text-label-primary">{description}</Paragraph>
</div>

// title takes what it needs, description gets the rest
<div className="grid flex-1 grid-cols-[auto_1fr] gap-1">
  <Paragraph className="font-medium">{title}</Paragraph>
  <Paragraph className="text-label-primary">{description}</Paragraph>
</div>
list-item.tsx
35Layout · Tailwind

min-w-0 stops a column blowing out

Grid and flex children default to min-width: auto, so one unbroken string can push the whole layout wider than its fr split.

<div className="grid grid-cols-[2fr_1fr] gap-1">
  {/* min-w-0 is important to prevent column from expanding
  if the user input long text without space */}
  <Textarea className="min-w-0" />
  <div>
    <Paragraph>{description}</Paragraph>
  </div>
</div>
form-row.tsx
38TypeScript · Generics

Type-safe field keys

Extract<keyof T, string> limits the argument to real string keys of T, so a typo in a field name fails at compile time.

type Item = {
  image: string,
  description: string,
}

const imageSelectorField = <T extends Record<string, unknown>>(
  key: Extract<keyof T, string>,
) => [
  `${key}.filename_disk`,
  `${key}.description`,
  `${key}.width`,
  `${key}.height`,
];

const getImageDetail = imageSelectorField<Item>("image") // Type Safe
const getImageDetail2 = imageSelectorField<Item>("unknown") // ERROR
lib/image-fields.ts
40Layout · Tailwind

Focus rings in a scroll container

overflow-y-auto clips focus rings and shadows at the edge. Pad the container and pull it back with a matching negative margin.

<div className="-m-0.75 flex max-h-60 flex-col gap-2 overflow-y-auto p-0.75">
  {data?.map(() => (<Button>...</Button>))}
</div>
option-list.tsx
41Config · npm

Pin ajv as a dev dependency

The usual fix for "Cannot find module 'ajv/dist/compile/codegen'": a transitive dependency wants ajv 8 and the tree resolved something older.

{
  "devDependencies": {
    "ajv": "^8.17.1"
  }
}
package.json
02Hook

useMediaQuery

Subscribes through useSyncExternalStore, so it never flashes the wrong branch during hydration.

import { useCallback, useSyncExternalStore } from 'react'

export function useMediaQuery(query: string) {
  const subscribe = useCallback((notify) => {
    const mql = window.matchMedia(query)
    mql.addEventListener('change', notify)
    return () => mql.removeEventListener('change', notify)
  }, [query])

  return useSyncExternalStore(
    subscribe,
    () => window.matchMedia(query).matches,
    () => false, // server snapshot
  )
}
use-media-query.ts
03Component

Segmented control

Real radio inputs under the pills, so arrow keys work and screen readers announce it as one group.

OverviewActivitySettings
export function Segmented({ options, name, value, onChange }) {
  return (
    <div role="radiogroup"
         className="inline-flex rounded-full bg-stone-200 p-1">
      {options.map((o) => (
        <label key={o}
          className="cursor-pointer rounded-full px-4 py-2
                     text-sm has-checked:bg-white">
          <input type="radio" name={name} value={o}
                 checked={value === o}
                 onChange={() => onChange(o)}
                 className="sr-only" />
          {o}
        </label>
      ))}
    </div>
  )
}
ui/segmented.tsx
06Shell · Docker

Reclaim a full disk on a box

What I run when a small EC2 instance fills up with old layers. Look before you prune. The last line deletes.

# where did it all go?
docker system df -v | head -20
du -sh /var/lib/docker/* | sort -h

# dangling images and stopped containers only
docker image prune -f
docker container prune -f

# everything not used by a running container
# (read that again before you run it)
docker system prune -af --volumes
disk-full.sh
07Component

CopyButton

The control on every card here. The confirmation state is the whole point, and aria-live announces it too.

CopyCopied ✓
'use client'
import { useState } from 'react'

export function CopyButton({ value }: { value: string }) {
  const [copied, setCopied] = useState(false)

  async function copy() {
    await navigator.clipboard.writeText(value)
    setCopied(true)
    setTimeout(() => setCopied(false), 1600)
  }

  return (
    <button onClick={copy} aria-live="polite">
      {copied ? 'Copied ✓' : 'Copy'}
    </button>
  )
}
ui/copy-button.tsx
10Utility · TanStack Query

Query option factories

Keys and fetchers defined once. Prefetch on the server and read on the client without the key drifting between them.

import { queryOptions } from '@tanstack/react-query'

export const eventQueries = {
  all: () => ['events'] as const,
  detail: (id: string) =>
    queryOptions({
      queryKey: [...eventQueries.all(), id],
      queryFn: () => fetchEvent(id),
      staleTime: 60_000,
    }),
}

// server: await qc.prefetchQuery(eventQueries.detail(id))
// client: useSuspenseQuery(eventQueries.detail(id))
queries/events.ts
11Utility · TypeScript

assertNever

Add a variant to the union and the compiler points at every switch that forgot it. Cheapest safety net in the language.

export function assertNever(value: never): never {
  throw new Error(`Unhandled case: ${JSON.stringify(value)}`)
}

type Status = 'queued' | 'running' | 'failed'

function label(s: Status) {
  switch (s) {
    case 'queued':  return 'Waiting'
    case 'running': return 'In progress'
    case 'failed':  return 'Failed'
    default: return assertNever(s)
  }
}
lib/assert-never.ts
14TypeScript · React

Typing refs and input handlers

A ref that is always set once mounted, and an onChange typed straight off the native element instead of hand-writing the event.

// useRef typescript
const anchor = useRef() as MutableRefObject<HTMLAnchorElement>;
const containerRef = useRef() as { current: HTMLDivElement };

// onChange typescript
import { ComponentProps } from "react"

const MyInput = ({
  onChange
}: {
  onChange: ComponentProps<"input">["onChange"]
}) => (
  <div>
    <input onChange={onChange} />
  </div>
);
my-input.tsx
15Layout · Tailwind

Equal-height siblings

items-stretch on the parent, h-auto on the children, and every card in the row matches the tallest one.

<div className="flex items-stretch">
  <div className="h-auto" />
  <div className="h-auto" />
</div>
layout.tsx
17Next.js · Image

next/image with fill

fill needs a relative parent with a size. Cover crops to the box, contain letterboxes inside it, and remote URLs work the same once the host is allowed.

import PersonImage from "@/src/assets/person-image.webp"

// cover: crop to fill the parent
<div className="relative">
  <Image src={PersonImage} fill className="h-full w-full object-cover" />
</div>

// contain: parent needs an explicit size
<div className="relative h-10 w-32">
  <Image src={PersonImage} fill className="h-full w-full object-contain" />
</div>

// remote source (host must be in next.config images)
<div className="relative">
  <Image src={`https://${BASE_URL}/person-image.webp`} fill className="h-full w-full object-cover" />
</div>
image.tsx
20Utility · Browser

Confirm before leaving the page

The browser shows its own generic prompt either way. Returning anything is what triggers it.

window.onbeforeunload = function () { return "Leave page?"; };
unsaved-changes.ts
21React · forwardRef

Using a forwarded ref inside the component

A forwarded ref can be a callback or an object. Narrow it before touching .current, or it breaks the day someone passes a function.

import { type ClassValue } from "clsx";
import { forwardRef, useCallback } from "react";

interface MainLayoutProps extends React.HTMLAttributes<HTMLDivElement> {
  backgroundColor?: ClassValue;
  isFullWidth?: boolean;
  isAddTopPadding?: boolean;
  isDisableMaxWidth?: boolean;
  isAddVerticalPadding?: boolean;
  isOverflowHidden?: boolean;
}

const MainLayout = forwardRef<HTMLDivElement, MainLayoutProps>(
  ({ ...props }, ref) => {
    const handleFocus = useCallback(() => {
      if (ref && typeof ref !== "function" && ref.current) {
        ref.current.focus();
      }
    }, [ref]);

    return (
      <div ref={ref} {...props}>
        {props.children}
      </div>
    );
  },
);

MainLayout.displayName = "MainLayout";
main-layout.tsx
23Component

Extending native element props

Take everything the native button takes, merge className through cn, spread the rest. Or pick a single prop's type off the element.

import { cn } from "@/lib/utils";

const CustomButton = ({
  className,
  children,
  ...rest
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => {
  return (
    <button className={cn("rounded-full", className)} {...rest}>
      {children}
    </button>
  );
};
export default CustomButton;

export const CustomDiv = ({
  className,
}: {
  className: React.HTMLAttributes<HTMLDivElement>["className"];
}) => {
  return <div className={className}>SomeDiv</div>;
};
ui/custom-button.tsx
25React · Motion

Driving variants by hand

useAnimationControls starts a variant from an event instead of from state, which is handy when the trigger is not a render.

const Component = () => {
  const animationControl = useAnimationControls();
  const motionVar: Variants = {
    animate: {
      opacity: 1,
    },
    exit: {
      opacity: 0,
    },
  };

  return (
    <motion.div
      variants={motionVar}
      animate={animationControl}
      onClick={() => animationControl.start("animate")}
      initial={"exit"}
    >
      Hello World
    </motion.div>
  );
};
component.tsx
27Layout · SVG

An SVG icon that will not size

If an icon ignores its button, give the svg h-full w-full and the button both h- and w-, not just one of them.

const PlainPlayIcon = () => (
  // add this class name for possible fix
  <svg
    fill="currentColor"
    viewBox="0 0 29 32"
    xmlns="http://www.w3.org/2000/svg"
    className="h-full w-full"
  >
    <path d="..." fill="currentColor" fillOpacity="0.85" />
  </svg>
);

const Icon = () => (
  // add both h-8 and w-8 for possible fix
  <button className="aspect-square h-8 w-8 rounded-full">
    <PlainPlayIcon />
  </button>
);
play-icon.tsx
29Config · Prisma

postinstall for Prisma and native deps

Regenerate the Prisma client and rebuild argon2 from source after every install, so a fresh CI box or a new arch does not ship a stale binary.

"scripts": {
  "postinstall": "prisma generate && npm rebuild argon2 --build-from-source"
}
package.json
31React · Refs

An array of refs

One ref per input for a PIN field, rebuilt from the length so focus can move to the next box.

const inputRefs = useRef<Array<RefObject<HTMLInputElement>>>([]);
inputRefs.current = Array.from({ length: pinLen }).map(
  (_, index) => (inputRefs.current[index] = createRef())
);
pin-input.tsx
33Component

Accordion item with a measured height

Animates max-height to the content's real height instead of a guessed number, so it opens smoothly for any amount of content.

"use client";

import { MutableRefObject, PropsWithChildren, useRef, useState } from "react";
import DropdownArrow from "../common/icons/DropdownArrow";

type Props = { title: string };

function DropdownItem({ title, children }: PropsWithChildren<Props>) {
  const [isOpen, setIsOpen] = useState(false);
  const itemRef = useRef() as MutableRefObject<HTMLDivElement>;

  return (
    <div className="rounded-lg bg-white p-4 shadow-[0px_2px_8px_0px_rgba(0,0,0,0.15)]">
      <div
        data-testid="dropdown-trigger"
        className="flex cursor-pointer items-center justify-between"
        onClick={() => setIsOpen((prev) => !prev)}
      >
        <p className="flex-1 py-2 pr-2 text-sm font-semibold sm:text-base">
          {title}
        </p>
        <div
          data-testid="dropdown-arrow"
          className={`${
            isOpen ? "rotate-180" : ""
          } flex aspect-square w-6 items-center justify-center transition-transform duration-200 ease-out`}
        >
          <DropdownArrow />
        </div>
      </div>
      <div
        className="overflow-hidden transition-all duration-500 ease-in-out"
        style={
          isOpen
            ? { maxHeight: itemRef.current.clientHeight }
            : { maxHeight: 0 }
        }
      >
        <div
          className={`border-t-solid flex h-full w-full flex-col gap-3 ${
            isOpen ? "border-opacity-100" : "border-opacity-0"
          } border-t-[1px] border-[#D2D2D2] pb-2 pt-4 text-sm transition-all duration-200 ease-out sm:text-base`}
          ref={itemRef}
        >
          {children}
        </div>
      </div>
    </div>
  );
}

export default DropdownItem;
dropdown-item.tsx
36CSS · Tailwind

Transition height to auto

interpolate-size: allow-keywords lets height animate from 0 to auto natively, no measuring, no max-height guess.

<div className="h-0 overflow-hidden pt-4 transition-all duration-300 ease-in-out [interpolate-size:allow-keywords] group-hover:h-auto">
  <p className="h5">{service.description}</p>
</div>
service-card.tsx
37TypeScript · Zod

Optional fields that accept an empty string

An empty form input sends "", which fails .email() and .url() even when the field is optional. .or(z.literal("")) lets it through.

export const profileFormSchema = z
  .object({
    profileEmail: z
      .email("invalidEmail")
      .max(255, "emailTooLong")
      .optional()
      .or(z.literal("")), // Prevent error on literal ""
    websiteUrl: z.url().max(255, "inputTooLong").optional().or(z.literal("")),
    customButtonUrl: z
      .url()
      .max(255, "inputTooLong")
      .optional()
      .or(z.literal("")),
  })
schemas/profile.ts
39Utility · Hook Form

API response to form defaults

One pure function between the DTO and the form. Returning DefaultValues<T> keeps it partial-safe and in step with the schema.

import type { DefaultValues } from "react-hook-form";
import type { QuestionItemDto } from "@/schema";
import type { CreateOkkAssessmentFormFields } from "../_static/schemas";

export default function transformOkkQuestionResponseToForm(
  questions?: QuestionItemDto[],
  currentLayer?: number
): DefaultValues<CreateOkkAssessmentFormFields> {
  return {
    assessments: questions?.map((question) => ({
      assessmentId: question.id,
      currentUserLayer: currentLayer,
      isRequireFile: !!question.isEvidenceRequired,
      deviationAction: question.deviationAction
    }))
  };
}
transform-okk-question.ts
42Component · Radix

Dialog without autofocus

Radix focuses the first focusable element on open, which pops the keyboard on mobile. Cancel it on the content.

<DialogContent
  onOpenAutoFocus={(e) => e.preventDefault()}
>
  {/* ... */}
</DialogContent>
dialog.tsx

Bookmarks.

8

Docs and answers I keep going back to.

Have something to ship?Let’s talk.

© 2026 Elian Richard
Elian Richard