Author Custom Breakpoints With defineMediaQuery() and Reuse Them Across Every Typed React Component
Media Queries and Breakpoints in React Styles
Creating Media Queries
With Salty CSS, you can define reusable media queries using the defineMediaQuery function:
// /styles/media.css.ts import { defineMediaQuery } from "@salty-css/react/config"; // Mobile breakpoints export const largeMobileDown = defineMediaQuery((media) => media.maxWidth(900)); export const smallMobileDown = defineMediaQuery((media) => media.maxWidth(400)); // Desktop breakpoints export const mediumDesktopDown = defineMediaQuery((media) => media.maxWidth(1440), ); export const smallDesktopDown = defineMediaQuery((media) => media.maxWidth(1100), ); // Feature-based media queries export const darkMode = defineMediaQuery((media) => media.prefersColorScheme("dark"), ); export const lightMode = defineMediaQuery((media) => media.prefersColorScheme("light"), );
Using Media Queries in Components
Once defined, you can use these media queries directly in your component styles:
import { styled } from "@salty-css/react/styled"; export const ResponsiveBox = styled("div", { base: { padding: "{spacing.large}", display: "grid", gridTemplateColumns: "1fr 1fr", gap: "{spacing.medium}", // Use media queries as properties prefixed with '@' "@mediumDesktopDown": { gridTemplateColumns: "1fr", gap: "{spacing.small}", }, // For small screens, adjust padding further "@largeMobileDown": { padding: "{spacing.medium}", }, // Even smaller screens "@smallMobileDown": { padding: "{spacing.small}", }, }, });
The media queries can be referenced directly by name with the @ prefix in your styles. This creates cleaner, more maintainable code compared to inline media queries.
Real-world Usage Example
Here's an actual example from this website's header component:
// From header.css.ts export const Navigation = styled("nav", { base: { margin: 0, display: "grid", gridAutoFlow: "column", gap: "{spacing.large}", alignItems: "center", // Progressive adjustment of spacing as screen size decreases "@mediumDesktopDown": { gap: "{spacing.medium}", }, "@smallDesktopDown": { gap: "{spacing.small}", }, }, }); export const Links = styled("ul", { base: { listStyle: "none", padding: 0, margin: 0, display: "flex", gap: "{spacing.medium}", // Transform to vertical menu on small screens "@smallDesktopDown": { display: "none", "&.open": { display: "flex", flexDirection: "column", // ...other mobile menu styles }, }, }, });
Available Media Query Methods
The media query builder provides several methods:
| Method/Property | Description |
|---|---|
minWidth(value) | Matches when viewport width is at least value |
maxWidth(value) | Matches when viewport width is at most value |
minHeight(value) | Matches when viewport height is at least value |
maxHeight(value) | Matches when viewport height is at most value |
prefersColorScheme(scheme) | Matches user's color scheme preference (dark or light) |
dark | Shorthand for prefersColorScheme("dark") |
light | Shorthand for prefersColorScheme("light") |
portrait | Matches when device is in portrait orientation |
landscape | Matches when device is in landscape orientation |
reducedMotion | Matches when user has requested reduced motion |
print | Targets print media type |
screen | Targets screen media type |
speech | Targets speech synthesizers |
all | Targets all device types |
not | Negates the media query |
custom(value) | Creates a custom media query with the provided value |
Combining Media Queries
You can combine multiple conditions using and() and or() methods:
// Match both conditions (tablet in portrait orientation) export const tabletPortrait = defineMediaQuery((media) => media .minWidth(768) .and(media.maxWidth(1024)) .and(media.orientation("portrait")), ); // Match either condition (dark mode OR mobile) export const darkOrMobile = defineMediaQuery((media) => media.prefersColorScheme("dark").or(media.maxWidth(480)), );
Three or more conditions
and() and or() chain — combine them freely for stricter matches:
// Mobile-sized device in portrait, with reduced-motion preference. export const mobileQuietPortrait = defineMediaQuery((media) => media .maxWidth(640) .and(media.orientation("portrait")) .and(media.reducedMotion), ); // Print OR small landscape (think: cheat-sheet layouts). export const printOrLandscapeMobile = defineMediaQuery((media) => media.print.or(media.maxWidth(640).and(media.orientation("landscape"))), );
The right-hand side of .and() / .or() accepts any media expression — including nested .and() / .or() calls — so you can build truth tables of any depth without giving up named exports.
Container queries
Container queries respond to the size of a nearby ancestor element rather than the viewport. Mark a container by setting containerType on it, then key off @container (...) inside its children's styles:
// /components/card.css.ts import { styled } from "@salty-css/react/styled"; export const Card = styled("article", { base: { containerType: "inline-size", // marks this element as a container padding: "1rem", // Style the children based on the container's width, not the viewport's. "@container (min-width: 480px)": { padding: "2rem", "& > *": { fontSize: "1.125rem" }, }, "@container (min-width: 768px)": { display: "grid", gridTemplateColumns: "1fr 2fr", gap: "1.5rem", }, }, });
Container queries respond to the size of the element that declares container-type, so the same Card can lay out differently in a sidebar (narrow container) versus a main column (wide container) — no JS measurement required.
Container queries make a single component lay out differently in a sidebar vs. a main column without any JS measurement — handy for design-system primitives that live in many slots.
Media Queries and Viewport Clamps
A common pattern: a viewport clamp for the base size, with the media query switching to a separate clamp tuned for a smaller reference screen:
import { styled } from "@salty-css/react/styled"; import { HDClamp, MobileClamp } from "../styles/helpers.css"; export const ResponsiveText = styled("h1", { base: { // Scale font size fluidly for larger screens fontSize: HDClamp(48), // Switch to mobile-optimized scaling at breakpoint "@largeMobileDown": { fontSize: MobileClamp(32), }, }, });
Best practices
Define all media queries in a central file — it's the only reliable way to keep names consistent across your component tree. Prefer semantic names (smallDesktopDown) over dimension-literal names (below1100) so the name stays accurate if the breakpoint value changes later. Combine with responsive tokens or viewport clamps for designs that scale fluidly rather than in hard steps.
Why isn't my media query applying?
If a @mediaName key isn't taking effect, run through these in order:
- Is the file imported in the build graph? A
defineMediaQueryexport needs to actually reach the bundler — re-export it from your styles barrel, or import it once fromsalty.config.ts. - Is the name spelled right at the call site? Salty CSS doesn't fail on unknown
@xxxkeys; they're emitted as literal at-rules and silently never match. Match the export name verbatim. - Is another rule winning by specificity? Inspect the element in DevTools — your
@mediaNamerule may be in the cascade but losing. Tighten the selector or bumppriorityon the styled component. - Are you nesting the media key inside the wrong scope?
@mediaNamebelongs as a key on a style object, not as a value:{ "@tabletDown": { padding: "1rem" } }, not{ padding: "@tabletDown 1rem" }.