SkillAgentSearch skills...

Fresnel

An SSR compatible approach to CSS media query based responsive layouts for React.

Install / Use

/learn @artsy/Fresnel
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

@artsy/fresnel

[![CircleCI][ci-icon]][ci] [![npm version][npm-icon]][npm]

The Fresnel equations describe the reflection of light when incident on an interface between different optical media.

– https://en.wikipedia.org/wiki/Fresnel_equations

Installation

  # React 18+
  yarn add @artsy/fresnel

  # React 17
  yarn add @artsy/fresnel@6

Table of Contents

Overview

When writing responsive components it's common to use media queries to adjust the display when certain conditions are met. Historically this has taken place directly in CSS/HTML:

@media screen and (max-width: 767px) {
  .my-container {
    width: 100%;
  }
}
@media screen and (min-width: 768px) {
  .my-container {
    width: 50%;
  }
}
<div class="my-container" />

By hooking into a breakpoint definition, @artsy/fresnel takes this declarative approach and brings it into the React world.

Basic Example

import React from "react"
import ReactDOM from "react-dom"
import { createMedia } from "@artsy/fresnel"

const { MediaContextProvider, Media } = createMedia({
  // breakpoints values can be either strings or integers
  breakpoints: {
    sm: 0,
    md: 768,
    lg: 1024,
    xl: 1192,
  },
})

const App = () => (
  <MediaContextProvider>
    <Media at="sm">
      <MobileApp />
    </Media>
    <Media at="md">
      <TabletApp />
    </Media>
    <Media greaterThanOrEqual="lg">
      <DesktopApp />
    </Media>
  </MediaContextProvider>
)

ReactDOM.render(<App />, document.getElementById("react"))

Server-side Rendering (SSR) Usage

The first important thing to note is that when server-rendering with @artsy/fresnel, all breakpoints get rendered by the server. Each Media component is wrapped by plain CSS that will only show that breakpoint if it matches the user's current browser size. This means that the client can accurately start rendering the HTML/CSS while it receives the markup, which is long before the React application has booted. This improves perceived performance for end-users.

Why not just render the one that the current device needs? We can't accurately identify which breakpoint your device needs on the server. We could use a library to sniff the browser user-agent, but those aren't always accurate, and they wouldn't give us all the information we need to know when we are server-rendering. Once client-side JS boots and React attaches, it simply washes over the DOM and removes markup that is unneeded, via a matchMedia call.

SSR Example

First, configure @artsy/fresnel in a Media file that can be shared across the app:

// Media.tsx

import { createMedia } from "@artsy/fresnel"

const ExampleAppMedia = createMedia({
  breakpoints: {
    sm: 0,
    md: 768,
    lg: 1024,
    xl: 1192,
  },
})

// Generate CSS to be injected into the head
export const mediaStyle = ExampleAppMedia.createMediaStyle()
export const { Media, MediaContextProvider } = ExampleAppMedia

Create a new App file which will be the launching point for our application:

// App.tsx

import React from "react"
import { Media, MediaContextProvider } from "./Media"

export const App = () => {
  return (
    <MediaContextProvider>
      <Media at="sm">Hello mobile!</Media>
      <Media greaterThan="sm">Hello desktop!</Media>
    </MediaContextProvider>
  )
}

Mount <App /> on the client:

// client.tsx

import React from "react"
import ReactDOM from "react-dom"
import { App } from "./App"

ReactDOM.render(<App />, document.getElementById("react"))

Then on the server, setup SSR rendering and pass mediaStyle into a <style> tag in the header:

// server.tsx

import React from "react"
import ReactDOMServer from "react-dom/server"
import express from "express"

import { App } from "./App"
import { mediaStyle } from "./Media"

const app = express()

app.get("/", (_req, res) => {
  const html = ReactDOMServer.renderToString(<App />)

  res.send(`
    <html>
      <head>
        <title>@artsy/fresnel - SSR Example</title>

        <!–– Inject the generated styles into the page head -->
        <style type="text/css">${mediaStyle}</style>
      </head>
      <body>
        <div id="react">${html}</div>

        <script src='/assets/app.js'></script>
      </body>
    </html>
  `)
})

app.listen(3000, () => {
  console.warn("\nApp started at http://localhost:3000 \n")
})

And that's it! To test, disable JS and scale your browser window down to a mobile size and reload; it will correctly render the mobile layout without the need to use a user-agent or other server-side "hints".

Usage with Gatsby or Next

@artsy/fresnel works great with Gatsby or Next.js's static hybrid approach to rendering. See the examples below for a simple implementation.

Example Apps

There are four examples one can explore in the /examples folder:

While the Basic and SSR examples will get one pretty far, @artsy/fresnel can do a lot more. For an exhaustive deep-dive into its features, check out the Kitchen Sink app.

If you're using Gatsby, you can also try gatsby-plugin-fresnel for easy configuration.

Why not conditionally render?

Other existing solutions take a conditionally rendered approach, such as [react-responsive][react-responsive] or [react-media][react-media], so where does this approach differ?

Server side rendering!

But first, what is conditional rendering?

In the React ecosystem a common approach to writing declarative responsive components is to use the browser’s [matchMedia api][match-media-api]:

<Responsive>
  {({ sm }) => {
    if (sm) {
      return <MobileApp />
    } else {
      return <DesktopApp />
    }
  }}
</Responsive>

On the client, when a given breakpoint is matched React conditionally renders a tree.

However, this approach has some limitations for what we wanted to achieve with our server-side rendering setup:

  • It's impossible to reliably know the user's current breakpoint during the server render phase since that requires a browser.

  • Setting breakpoint sizes based on user-agent sniffing is prone to errors due the inability to precisely match device capabilities to size. One mobile device might have greater pixel density than another, a mobile device may fit multiple breakpoints when taking device orientation into consideration, and on desktop clients there is no way to know at all. The best devs can do is guess the current breakpoint and populate <Responsive> with assumed state.

Artsy settled on what we think makes the best trade-offs. We approach this problem in the following way:

  1. Render markup for all breakpoints on the server and send it down the wire.

  2. The browser receives markup with proper media query styling and will immediately start rendering the expected visual result for whatever viewport width the browser is at.

  3. When all JS has loaded and React starts the rehydration phase, we query the browser for what breakpoint it’s currently at and then limit the rendered components to the matching media queries. This prevents life-cycle methods from firing in hidden components and unused html being re-written to the DOM.

  4. Additionally, we register event listeners with the browser to notify the MediaContextProvider when a different breakpoint is matched and then re-render the tree using the new value for the onlyMatch prop.

Let’s compare what a component tree using matchMedia would look like with our approach:

<table> <tr><th>Before</th><th>After</th></tr> <tr><td>
<Responsive>
  {({ sm }) => {
    if (sm) return <SmallArticleItem {...props} />
    else return <LargeArticleItem {...props} />
  }}
</Responsive>
</td> <td>
<>
  <Media at="sm">
    <SmallArticleItem {...props} />
  </Media>
  <Media greaterThan="sm">
    <LargeArticleItem {...props} />
  </Media>
</>
</td></tr> </table>

See the server-side rendering app for a working example.

API

createMedia

First things first. You’ll need to define the breakpoints and interaction needed for your design to produce the set of media components you can use throughout your application.

For example, consider an application that has the following breakpoints:

  • A viewport width between 0 and 768 (768 not included) points, named sm.
  • A viewport width between 768 and 1024 (1024 not included) points, named md.
  • A viewport width between 1024 and 1192 (1192 not included) points, named lg.
  • A viewport width from 1192 points and above, named xl.

And the following interactions:

  • A device that supports hovering a pointer device, named hover.
  • A device that does not support hovering a pointer device, named notHover.

You would then produce the set of media components like so:

// Media.tsx

const ExampleAppMedia = createMedia({
  breakpoints: {
    sm: 0,
    md: 768,
    lg: 1024,
    xl: 1192,
  },
  interactions: {
    hover: "(hover: hover)",
    notHover: "(hover: none)",
    landscape: "not all and (orientation: landscape)",
    portrait: "not all and (orientation: portrait)",
  },
})

export const { Media, MediaContextProvider, createMediaStyle } = ExampleAppMedia

As you can see, breakpoints are defined by their start offset, where the first one is expected to start at 0.

MediaContextProvider

The MediaContextProvider component influenc

View on GitHub
GitHub Stars1.3k
CategoryDevelopment
Updated6d ago
Forks67

Languages

TypeScript

Security Score

85/100

Audited on Mar 25, 2026

No findings