0.3.2 • Published 1 month ago

mezz v0.3.2

Weekly downloads
-
License
MIT
Repository
github
Last release
1 month ago

Mezz

📐 Mezz is a set of react hooks for building responsive and adaptable web interfaces.

These hooks observe the size of elements and match the breakpoints you supply — simple, type-safe, and fast.

  • 💪 Type-safe breakpoint auto-completions
  • ✨ Uses the modern ResizeObserver browser API
  • ⚡️ Customizable breakpoint naming
  • 🌐 Works in all modern browsers

The hooks

These hooks have similarities to @container queries in css:

  • 🪝 useWidth - Observe the width of a jsx element (simplest api)
  • 🪝 useWidthHeight - Observe the width and height of a jsx element

And this hook works like a traditional @media query:

  • 🪝 useBodyWidth - Observe the width of document.body

Why not use window.matchMedia?

Mezz recognizes that elements can change size dynamically without the user resizing the window. For instance, interactions like opening and closing sidebars can cause adjacent content to adjust its width.

Unlike alternative size matching libraries built with window.matchMedia, which only matches full browser window sizes, Mezz allows developers to observe and respond to the size of specific elements within the DOM.

Why not use container queries?

While container queries enable styling within CSS, they lack the capability to conditionally render components or map to component props in TypeScript.

For instance, hiding content with CSS still renders it in the browser, impacting performance. Mezz, on the other hand, allows for conditional rendering based on container size, potentially improving performance by avoiding unnecessary rendering and layout calculations.

Getting started

npm install mezz

And then import one of these hooks:

useWidth

Observe the width of a jsx element:

import { useWidth } from 'mezz'

function App() {
  const box = useWidth({ lg: 700 })
  return (
    <div ref={box.ref}>
      <p>Active breakpoint: {box.active}</p>
      {'// Use as a conditional'}
      {box.lg ? <LargeView /> : <SmallView />}
      {'// Or as a prop'}
      <Sidebar isPinned={box.lg} />;
    </div>
  )
}

Add more breakpoints for each of your component width sizes:

import { useWidth } from 'mezz'

function App() {
  const box = useWidth({ sm: 0, md: 400, lg: 600 })
  return (
    <>
      <p>Active breakpoint: {box.active}</p>
      {box.sm && <SmallView />}
      {box.md && <MediumView />}
      {box.lg && <LargeView />}
    </>
  )
}

Breakpoints can be named xxs/xs/sm/md/lg/xl/xxl or customized (see Customisation).

Add a custom throttle value to control the number of times the breakpoint is updated:

const box = useWidth(
  { lg: 600 },
  { throttleWait: 500 } // Default value
)

useWidthHeight

Observe the width and height of a jsx element:

import { useWidthHeight } from 'mezz'

function App() {
  const box = useWidthHeight({
    width: { sm: 0, lg: 800 },
    height: { lg: 500 },
  })
  return (
    <div ref={box.ref}>
      <p>Active width breakpoint: {box.width.active}</p>
      <p>Active height breakpoint: {box.height.active}</p>
      {'// Use as a conditional'}
      {box.width.sm && <SmallView />}
      {box.width.lg && <LargeView isExpanded={box.height.lg} />}
      {'// Or as a prop'}
      <Item hasOverlay={!box.width.lg} />;
    </div>
  )
}

Rather than using the produced ref, you can also pass in an existing ref:

const myRef = useRef(null)
const box = useWidthHeight({ width: { lg: 800 }, ref: myRef })

Add a custom throttle value to control the number of times the breakpoint is updated:

const box = useWidthHeight({
  width: { lg: 800 },
  throttleWait: 500, // Default value
})

useBodyWidth

Observe the width of document.body and change layout based on a breakpoint of 500px body width:

import { useBodyWidth } from 'mezz'

function App() {
  const body = useBodyWidth({ lg: 500 })
  return (
    <>
      <p>Active breakpoint: {body.active}</p>
      {'// Use as a conditional'}
      {body.lg ? <LargeLayout /> : <SmallLayout />}
      {'// Or as a prop'}
      <MyComponent hasOverlay={!body.lg} />;
    </>
  )
}

Add more sizes for each of your body-width breakpoints:

import { useBodyWidth } from 'mezz'

function App() {
  const body = useBodyWidth({ sm: 0, md: 400, lg: 600 })
  return (
    <>
      <p>Active breakpoint: {body.active}</p>
      {body.sm && <SmallLayout />}
      {body.md && <MediumLayout />}
      {body.lg && <LargeLayout />}
    </>
  )
}

Add a custom throttle value to control the number of times the breakpoint is updated:

const body = useBodyWidth(
  { lg: 600 },
  { throttleWait: 500 } // Default value
)

Customisation

Breakpoint naming

Out-of-the-box mezz offers these breakpoint names to use: xxs/xs/sm/md/lg/xl/xxl.

Customize by wrapping any of the hooks and passing in your own breakpoint names:

Using useBodyWidth

import { useBodyWidth, CustomBreakpoint } from 'mezz'

type BPoints = 'small' | 'medium' | 'large'

const useBodySize = <W extends BPoints>(
  params: CustomBreakpoint<W> & { active?: boolean }
) => useBodyWidth<W>(params)

function App() {
  const body = useBodySize({ small: 0, medium: 400, large: 800 })
  return (
    <div>
      <p>Active breakpoint: {body.active}</p>
      {body.small && <SmallLayout />}
      {body.medium && <MediumLayout />}
      {body.large && <LargeLayout />}
    </div>
  )
}

Using useWidthHeight

import { useWidthHeight, CustomBreakpoint } from 'mezz'

type BPoints = 'small' | 'medium' | 'large'

const useBreakpoints = <W extends BPoints, H extends BPoints>(params: {
  width: CustomBreakpoint<W>
  height: CustomBreakpoint<H>
}) => useWidthHeight<W, H>(params)

function App() {
  const bp = useBreakpoints({
    width: { small: 0, large: 1024 },
    height: { large: 500 },
  })
  return (
    <div ref={bp.ref}>
      <p>Active width breakpoint: {bp.width.active}</p>
      <p>Active height breakpoint: {bp.height.active}</p>
      {bp.width.small && <SmallView />}
      {bp.width.large && <LargeView isExpanded={bp.height.large} />}
    </div>
  )
}

Syncing screens with Tailwind

Mezz can be used with Tailwind CSS to match the same breakpoints you've defined in your tailwind.config.js file.

Here’s how to sync the hook useBodyWidth with your Tailwind screens config.

First, we need to extract and export the screens object from tailwind.config.js. You’ll need to define your screens in the below format:

// tailwind.config.ts
export const screens = {
  sm: '640px',
  md: '768px',
  lg: '1024px',
  xl: '1280px',
  custom: '1536px',
} as const

const config = {
  theme: {
    screens,
    // ...
  },
  // ...
} satisfies Config

export default config

Next we create a provider and use the screens object to generate the breakpoint config:

// breakpointProvider.tsx
import { useContext, createContext, useMemo } from 'react'
import { useBodyWidth } from 'mezz'
import { getTailwindBodyWidthConfig } from 'mezz/tailwind'
import { screens } from '../../tailwind.config'
import type { ReactNode } from 'react'

const mezzBodyWidthConfig = getTailwindBodyWidthConfig(screens)

type BodyWidthKeys = keyof typeof mezzBodyWidthConfig
type BreakpointContextType = ReturnType<typeof useBodyWidth<BodyWidthKeys>>

export const BreakpointProvider = (props: { children: ReactNode }) => {
  const cachedMezzConfig = useMemo(() => mezzBodyWidthConfig, [])
  const size = useBodyWidth(cachedMezzConfig)

  return (
    <BreakpointContext.Provider value={size}>
      {props.children}
    </BreakpointContext.Provider>
  )
}

const BreakpointContext = createContext<BreakpointContextType | null>(null)

export const useBreakpoint = () => {
  const context = useContext(BreakpointContext)
  if (!context) {
    throw new Error('useBreakpoint must be used within a BreakpointProvider')
  }

  return context
}

Next, wrap your app with the BreakpointProvider above.

Then use your tailwind body width context like this:

const Component = () => {
  const bp = useBreakpoint()
  //    ^? const bp: { active: "sm" | "md" | "lg" | "xl" | "custom" | null; }
  //       & Record<"sm" | "md" | "lg" | "xl" | "custom", boolean>

  return <>{!bp.xs && `I'm not small`}</>
}
0.3.2

1 month ago

0.3.1

1 month ago

0.3.0

2 months ago

0.2.0

2 months ago

0.1.2

6 months ago

0.1.1

9 months ago

0.1.0

9 months ago