Domphy

Responsive Design

CSS breakpoints

Domphy has no JavaScript breakpoint API — use standard CSS media queries directly in style objects or <style> tags:

// Inline style with CSS string for media queries
const Layout = {
  div: [Sidebar, Content],
  style: `
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;

    @media (min-width: 768px) {
      grid-template-columns: 240px 1fr;
    }
    @media (min-width: 1280px) {
      grid-template-columns: 280px 1fr 240px;
    }
  `,
}

Standard breakpoints (Material Design 3 / Bootstrap aligned):

NameWidthTypical target
sm≥ 480pxLarge phones
md≥ 768pxTablets
lg≥ 1024pxSmall desktops
xl≥ 1280pxDesktops
2xl≥ 1536pxLarge monitors

Reactive breakpoints with matchMedia

For JavaScript-driven layout changes (e.g. mounting different components on mobile vs desktop), observe matchMedia:

import { toState } from "@domphy/core"

const isMobile = toState(window.matchMedia("(max-width: 767px)").matches)

const mql = window.matchMedia("(max-width: 767px)")
mql.addEventListener("change", (e) => isMobile.set(e.matches))

// Use in components
const Navigation = {
  nav: (l) => isMobile.get(l) ? MobileNav : DesktopNav,
}

Utility: createBreakpoint

Create a reusable reactive breakpoint helper:

import { toState } from "@domphy/core"

function createBreakpoint(query: string) {
  const state = toState(window.matchMedia(query).matches)
  const mql = window.matchMedia(query)
  mql.addEventListener("change", (e) => state.set(e.matches))
  return state
}

const isMd = createBreakpoint("(min-width: 768px)")
const isLg = createBreakpoint("(min-width: 1024px)")
const isDark = createBreakpoint("(prefers-color-scheme: dark)")

const Header = {
  header: (l) => isLg.get(l) ? FullHeader : CompactHeader,
}

Container queries

Container queries respond to the container's width rather than the viewport — ideal for reusable components:

// Define a container
const Card = {
  div: CardContent,
  style: {
    containerType: "inline-size",
    containerName: "card",
  },
}
/* CSS for responsive card layout */
.card-content { display: block; }

@container card (min-width: 400px) {
  .card-content {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 1rem;
  }
}

Adaptive navigation

Mobile-first navigation pattern — collapse to a hamburger on small screens:

import { toState } from "@domphy/core"
import { button } from "@domphy/ui"

const menuOpen = toState(false)

const MobileMenuButton = {
  button: (l) => menuOpen.get(l) ? "✕" : "☰",
  $: [button()],
  onClick: () => menuOpen.set((v) => !v),
  style: { display: "none", "@media(max-width:767px)": { display: "flex" } },
}

const NavLinks = {
  ul: [
    { li: { a: "Home", href: "/" } },
    { li: { a: "Docs", href: "/docs" } },
  ],
  style: (l) => ({
    display: menuOpen.get(l) ? "flex" : "none",
    "@media(min-width:768px)": { display: "flex" },
    flexDirection: "column",
    "@media(min-width:768px)": { flexDirection: "row" },
  }),
}

Adaptive images

Use srcset and sizes for responsive images — no JavaScript needed:

const HeroImage = {
  img: null,
  src: "/hero-800.jpg",
  srcset: "/hero-400.jpg 400w, /hero-800.jpg 800w, /hero-1600.jpg 1600w",
  sizes: "(max-width: 768px) 100vw, (max-width: 1280px) 50vw, 800px",
  alt: "Hero",
  loading: "lazy",
  style: { width: "100%", height: "auto" },
}

Hiding elements on mobile/desktop

Use hidden reactively:

const DesktopSidebar = {
  aside: SidebarContent,
  hidden: (l) => !isLg.get(l),
}

const MobileDrawer = {
  div: SidebarContent,
  hidden: (l) => isLg.get(l),
}

Or pure CSS:

const DesktopOnlyColumn = {
  div: Content,
  style: { "@media(max-width:767px)": { display: "none" } },
}

Fluid spacing

Scale padding/margins with viewport width using clamp():

const Section = {
  section: Content,
  style: {
    padding: "clamp(1rem, 5vw, 4rem)",   // 16px → 64px as viewport grows
  },
}

Dark mode

Respond to the user's system preference:

const prefersDark = createBreakpoint("(prefers-color-scheme: dark)")

// Apply theme based on system preference (if user hasn't manually set one)
prefersDark.subscribe((dark) => {
  const saved = localStorage.getItem("dp-theme")
  if (!saved) {
    document.documentElement.setAttribute("data-theme", dark ? "dark" : "light")
  }
})

Or let the RUNTIME_SCRIPT handle it — the script in @domphy/press (and apps/web/html-template.ts) reads localStorage and applies the saved theme on first load.

SSR considerations

window.matchMedia is not available in Node.js. Guard breakpoint code:

const isClient = typeof window !== "undefined"

const isMobile = toState(
  isClient ? window.matchMedia("(max-width: 767px)").matches : false
)

if (isClient) {
  const mql = window.matchMedia("(max-width: 767px)")
  mql.addEventListener("change", (e) => isMobile.set(e.matches))
}

With @domphy/app SSR, breakpoints should be resolved client-side via hydration — avoid rendering different HTML on server vs client to prevent hydration mismatches.