DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Build iOS Settings from scratch: a data-driven list, a real toggle, and a push-navigation stack

The screen behind every app's gear icon looks deceptively simple: rounded cards of rows, an uppercase header, a grey footnote. But that inset-grouped list is a small, complete UI system — a data-driven renderer, four kinds of trailing accessory, and an in-panel navigation stack where a choice made deep in a subpage flows back to the row that sent you. Here's how to build it in vanilla HTML, CSS and JS, no framework.

Describe the whole screen as data

The first rule: don't hand-write rows. Describe every screen as a model — a map of screens, each a list of groups, each group a list of row descriptors. A descriptor names its type, its icon and tint, the state key it reads and writes, and for a chevron row the screen it pushes plus a value function of state.

const SCREENS = {
  root: { title: "Settings", groups: [
    { footer: "Airplane Mode turns off Wi-Fi and Bluetooth.", rows: [
      { type: "toggle", key: "airplane", label: "Airplane Mode", icon: "airplane", tint: "#ff9500" },
      { type: "nav", label: "Wi-Fi", icon: "wifi", tint: "#007aff", screen: "wifi",
        value: s => s.airplane ? "Off" : (s.wifiOn ? s.network : "Off") },
    ]},
  ]},
  display: { title: "Display & Brightness", groups: [ /* … */ ] },
};
Enter fullscreen mode Exit fullscreen mode

One data structure, one renderer. Adding a setting is adding a map entry, never writing markup.

The toggle switch that can't lie

The iOS switch is a real <button> with role="switch" and aria-checked. The knob is a circle moved by a CSS transform transition, and the class .on both colours the track and slides the knob — so the visual state and the announced state come from one boolean and can never disagree.

const sw = document.createElement("button");
sw.setAttribute("role", "switch");
sw.className = "s-switch" + (state[r.key] ? " on" : "");
sw.setAttribute("aria-checked", String(!!state[r.key]));
sw.onclick = () => {
  state[r.key] = !state[r.key];                 // flip the source of truth
  sw.classList.toggle("on", state[r.key]);      // .on slides the knob (CSS)
  sw.setAttribute("aria-checked", String(state[r.key]));
  onStateChange();                              // repaint everything that reads it
};
Enter fullscreen mode Exit fullscreen mode

Navigation is an array of screen ids

The clever part is the push stack. Navigation is just an array of screen ids; the DOM is a horizontal strip of full-width panels, and you show depth d by translating the strip -d × 100%. Push builds a fresh panel, appends it, then shifts — it slides in. Pop shifts back, then removes the panel after the transition ends.

let nav = ["root"];
function push(id){
  nav.push(id);
  stack.append(renderScreen(id));                 // fresh panel off to the right
  stack.style.transform = `translateX(-${(nav.length-1)*100}%)`;
  syncNavbar();
}
Enter fullscreen mode Exit fullscreen mode

The touch that makes an iOS back button feel oriented: it's labelled with the previous screen's title, not a generic "Back."

function syncNavbar(){
  const top = nav[nav.length - 1], prev = nav[nav.length - 2];
  title.textContent = SCREENS[top].title;
  if (prev){
    back.classList.add("show");
    backLabel.textContent = SCREENS[prev].title;    // "‹ Settings"
  } else back.classList.remove("show");             // root: nothing behind
}
Enter fullscreen mode Exit fullscreen mode

One state object, one repaint

Every control — switch, slider, checkmark — writes to a single state. After any write, onStateChange() fans out: recolour the panel for Dark, scale labels for Text Size, dim for Brightness, recompute the nav values so a deep pick flows back to its parent row, and repaint the live preview. Because a chevron row's value is a function of state (stashed on the element so it can be recomputed in place), the Wi-Fi row instantly shows the network you picked two screens deep.

That's the whole pattern behind a hundred settings panes: inset groups of rows, a trailing accessory that tells you what a row does (switch = instant on/off, chevron = more behind it, checkmark = one of many, slider = a range), and a push stack that trades depth for a flat, uncluttered screen.

Flip Dark, drag Text Size, pick a network deep in a subpage and watch the value flow back — all live at: https://dev48v.infy.uk/design/day54-apple-settings.html

Top comments (0)