# iOS Stocks

> Dark quotes with area chart, sparklines and three display modes.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/templates/ios-stocks
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/ios-stocks.json
```

Adds the page at `app/ios-stocks/page.tsx` together with the components below. Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Components

- [iOS Kit](https://chromotion.dev/r/ios-kit.md): `@/components/ios-kit`

## Dependencies

```bash
npm install lucide-react
```

## Source

```tsx title="app/ios-stocks/page.tsx"
"use client"

import * as React from "react"
import { Newspaper } from "lucide-react"

import { cn } from "@/lib/utils"
import { IOSDevice, IOSPage, IOSSegmented } from "@/components/ios-kit"

type Stock = { symbol: string; name: string; price: number; change: number; cap: string; series: number[] }

const stocks: Stock[] = [
  { symbol: "AAPL", name: "Apple Inc.", price: 227.48, change: 1.24, cap: "3.45T", series: [221, 222, 221.5, 224, 223, 225.5, 226, 225, 227, 227.5] },
  { symbol: "MSFT", name: "Microsoft Corp.", price: 418.12, change: -0.56, cap: "3.11T", series: [424, 423, 424.5, 421, 422, 419, 420, 418.5, 419, 418] },
  { symbol: "NVDA", name: "NVIDIA Corp.", price: 138.9, change: 3.42, cap: "3.40T", series: [128, 130, 129, 132, 134, 133, 136, 137, 138, 139] },
  { symbol: "TSLA", name: "Tesla, Inc.", price: 251.07, change: -2.18, cap: "802B", series: [262, 260, 261, 258, 257, 255, 256, 253, 252, 251] },
  { symbol: "AMZN", name: "Amazon.com", price: 195.33, change: 0.71, cap: "2.04T", series: [192, 193, 192.5, 194, 193.5, 194.5, 195, 194.8, 195.2, 195.3] },
]

const ranges = ["1D", "1W", "1M", "3M", "1Y"]
const display = ["Percent", "Price", "Market Cap"]

function path(series: number[], w: number, h: number) {
  const min = Math.min(...series)
  const max = Math.max(...series)
  const span = max - min || 1
  return series
    .map((v, i) => `${i === 0 ? "M" : "L"}${((i / (series.length - 1)) * w).toFixed(1)} ${(h - ((v - min) / span) * (h - 4) - 2).toFixed(1)}`)
    .join(" ")
}

function Spark({ series, up }: { series: number[]; up: boolean }) {
  return (
    <svg viewBox="0 0 64 28" className="h-7 w-16" aria-hidden>
      <path d={path(series, 64, 28)} fill="none" strokeWidth="1.8" strokeLinejoin="round" strokeLinecap="round" className={up ? "stroke-(color:--ios-green)" : "stroke-(color:--ios-red)"} />
    </svg>
  )
}

export default function IOSStocks() {
  const [selected, setSelected] = React.useState("AAPL")
  const [range, setRange] = React.useState(0)
  const [mode, setMode] = React.useState(0)
  const stock = stocks.find((s) => s.symbol === selected) ?? stocks[0]
  const up = stock.change >= 0
  const series = React.useMemo(() => {
    const step = Math.max(1, 5 - range)
    return stock.series.map((v, i) => v * (1 + Math.sin((i + range) * (range + 1)) * 0.004 * step))
  }, [stock, range])

  const label = (s: Stock) => (mode === 0 ? `${s.change > 0 ? "+" : ""}${s.change.toFixed(2)}%` : mode === 1 ? `${s.change > 0 ? "+" : ""}${((s.price * s.change) / 100).toFixed(2)}` : s.cap)

  return (
    <IOSDevice light className="bg-black text-white [--ios-bg:#000000] [--ios-label:#ffffff] [--ios-label2:#ebebf599] [--ios-label3:#ebebf54d] [--ios-sep:#54545aa6] [--ios-surface:#1c1c1e] [--ios-fill:#7676803d] [--ios-thumb:#636366]">
      <IOSPage title="Stocks" right={<span className="text-[15px] font-semibold text-(color:--ios-blue)">Edit</span>}>
        <p className="-mt-2 px-5 pb-2 text-[22px] font-bold text-(color:--ios-label2)">Nov 7</p>

        <section aria-label={`${stock.symbol} chart`} className="mx-4 mb-3 rounded-[16px] bg-(color:--ios-surface) p-4">
          <div className="flex items-baseline justify-between">
            <div>
              <h2 className="text-[17px] font-semibold">{stock.symbol}</h2>
              <p className="text-[13px] text-(color:--ios-label2)">{stock.name}</p>
            </div>
            <div className="text-right">
              <p className="text-[28px] leading-8 font-semibold tabular-nums">{stock.price.toFixed(2)}</p>
              <p className={cn("text-[14px] font-medium tabular-nums", up ? "text-(color:--ios-green)" : "text-(color:--ios-red)")}>
                {up ? "+" : ""}{stock.change.toFixed(2)}%
              </p>
            </div>
          </div>
          <svg viewBox="0 0 320 120" className="mt-3 h-[120px] w-full" role="img" aria-label={`${stock.symbol} price over ${ranges[range]}`}>
            <defs>
              <linearGradient id="ios-stock-fill" x1="0" x2="0" y1="0" y2="1">
                <stop offset="0%" stopColor={up ? "#30d158" : "#ff453a"} stopOpacity="0.35" />
                <stop offset="100%" stopColor={up ? "#30d158" : "#ff453a"} stopOpacity="0" />
              </linearGradient>
            </defs>
            <path d={`${path(series, 320, 120)} L320 120 L0 120Z`} fill="url(#ios-stock-fill)" />
            <path d={path(series, 320, 120)} fill="none" strokeWidth="2" strokeLinejoin="round" strokeLinecap="round" stroke={up ? "#30d158" : "#ff453a"} />
          </svg>
          <div role="tablist" aria-label="Range" className="mt-2 flex justify-between">
            {ranges.map((r, i) => (
              <button
                key={r}
                type="button"
                role="tab"
                aria-selected={range === i}
                onClick={() => setRange(i)}
                className={cn("rounded-full px-3 py-1 text-[13px] font-semibold transition-colors", range === i ? "bg-white/20 text-white" : "text-(color:--ios-label2)")}
              >
                {r}
              </button>
            ))}
          </div>
        </section>

        <div className="px-4 pb-2">
          <IOSSegmented options={display} value={mode} onValueChange={setMode} />
        </div>

        <ul>
          {stocks.map((s) => {
            const isUp = s.change >= 0
            return (
              <li key={s.symbol} className="relative px-4 after:absolute after:right-0 after:bottom-0 after:left-4 after:h-px after:bg-(color:--ios-sep)">
                <button
                  type="button"
                  onClick={() => setSelected(s.symbol)}
                  aria-pressed={selected === s.symbol}
                  className={cn("-mx-4 flex w-[calc(100%+2rem)] items-center gap-3 px-4 py-3 text-left active:bg-white/5", selected === s.symbol && "bg-white/5")}
                >
                  <span className="min-w-0 flex-1">
                    <span className="block text-[17px] leading-[22px] font-semibold">{s.symbol}</span>
                    <span className="block truncate text-[14px] text-(color:--ios-label2)">{s.name}</span>
                  </span>
                  <Spark series={s.series} up={isUp} />
                  <span className="flex w-[130px] items-center justify-end gap-2">
                    <span className="text-[17px] tabular-nums">{s.price.toFixed(2)}</span>
                    <span className={cn("w-[74px] rounded-md py-1 text-center text-[15px] font-medium tabular-nums", isUp ? "bg-(color:--ios-green) text-neutral-950" : "bg-[#d70015] text-white")}>
                      {label(s)}
                    </span>
                  </span>
                </button>
              </li>
            )
          })}
        </ul>

        <div className="mx-4 mt-5 flex items-center gap-3 rounded-[16px] bg-(color:--ios-surface) p-4">
          <Newspaper className="size-6 shrink-0 text-(color:--ios-red)" />
          <div>
            <p className="text-[12px] font-semibold text-(color:--ios-label2) uppercase">Top Stories</p>
            <p className="text-[15px] leading-5 font-medium">Chipmakers lead a broad rally as investors price in lower rates.</p>
          </div>
        </div>
        <div className="h-10" />
      </IOSPage>
    </IOSDevice>
  )
}
```
