惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
Blog — PlanetScale
Blog — PlanetScale
B
Blog RSS Feed
L
LangChain Blog
Jina AI
Jina AI
爱范儿
爱范儿
C
Check Point Blog
云风的 BLOG
云风的 BLOG
Last Week in AI
Last Week in AI
月光博客
月光博客
GbyAI
GbyAI
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Stack Overflow Blog
Stack Overflow Blog
V
V2EX
A
About on SuperTechFans
有赞技术团队
有赞技术团队
Microsoft Azure Blog
Microsoft Azure Blog
The GitHub Blog
The GitHub Blog
博客园 - Franky
Apple Machine Learning Research
Apple Machine Learning Research
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Why your React tournament bracket breaks in Safari (and a...
Hojayfa Rahman · 2026-05-31 · via DEV Community

You build a tournament bracket with a popular React library. In Chrome it's perfect — neat columns, clean connector lines. Then you open it on an iPhone, or in Safari, or inside your Capacitor app… and every match is crammed into the top-left corner, stacked on top of the round headers.

If you've ever shipped a bracket to iOS, you've probably seen this exact bug. Here's why it happens — and a tiny library that fixes it for good.

The symptom

It looks fine everywhere Chromium runs (Chrome, Edge, Android WebView) and completely broken everywhere WebKit runs:

  • Safari (macOS and iOS)
  • iOS WKWebView
  • Capacitor / Cordova apps
  • Electron-on-WebKit

The matches don't just shift a little — they all render at coordinate (0,0) of the bracket, piling on top of each other and the headers.

The cause: SVG <foreignObject> in WebKit

Most React bracket libraries — @g-loot/react-tournament-brackets, react-tournament-bracket, and friends — render the bracket as an SVG and place each match's HTML inside a <foreignObject> positioned with x/y attributes.

WebKit has a long-standing bug: it ignores x, y, and transform on <foreignObject> and positions the content relative to the top-level <svg> instead of the foreignObject's own coordinates. Every match therefore collapses to the origin.

And there's no CSS escape hatch — x, y, and transform are all ignored on foreignObject in Safari, so you can't nudge the content back into place. I even tried patching a library to wrap each match in a <g transform="translate(x,y)"> instead of a nested <svg x y>; WebKit ignores ancestor transforms for foreignObject positioning too. The SVG approach is simply a dead end on WebKit.

The fix: don't use SVG at all

A bracket is really just columns of cards joined by connector lines — and both are expressible in plain CSS.

Here's the key insight. Put each round in a flex column where every match sits in an equal flex: 1 slot. Because each round has half the matches of the previous one, a match's slot spans exactly two feeder slots — so the two feeders land at 25% and 75% of that slot, and the match itself at 50%:

Round 1 slot   Round 2 slot
┌──────────┐
│ Match A  │──┐   25%
├──────────┤  ├─►┌──────────┐
│ Match B  │──┘  │ Winner   │   50%
└──────────┘     └──────────┘
┌──────────┐
│ Match C  │──┐   75%
│   ...    │

Draw the connector elbow with a few absolutely-positioned, bordered <div>s at those same 25 / 50 / 75% offsets, and the tree stays perfectly aligned at any height — with zero JavaScript measuring and no SVG. Because it's all flexbox and borders, it renders identically on Chromium, Firefox, and WebKit.

bracketkit

I packaged this up as bracketkit — a headless, pure-CSS tournament bracket for React:

  • 🍏 Works in Safari / WebKit — no SVG, no foreignObject.
  • 🧩 Headlessyou render the match card; bracketkit owns layout + connectors. No theme objects, no design lock-in.
  • 🪶 ~4 KB, zero dependencies — ESM + CJS + first-class TypeScript types.
  • SSR-safe — no measurement, correct on the first server render.
  • 🎨 Style it any way — plain CSS, Tailwind, or a drop-in shadcn/ui component.

Quick start

npm i bracketkit

import { Bracket, type BracketRound } from "bracketkit"

type Match = { id: string; home: string; away: string; homeScore?: number; awayScore?: number }

const rounds: BracketRound<Match>[] = [
  {
    id: "sf",
    name: "Semi-finals",
    matches: [
      { id: "sf1", home: "Lions", away: "Bears", homeScore: 2, awayScore: 1 },
      { id: "sf2", home: "Hawks", away: "Wolves", homeScore: 0, awayScore: 3 },
    ],
  },
  { id: "f", name: "Final", matches: [{ id: "f1", home: "Lions", away: "Wolves" }] },
]

export function Playoffs() {
  return (
    <div style={{ overflowX: "auto", color: "#64748b" /* connector color */ }}>
      <Bracket
        rounds={rounds}
        renderRoundHeader={(round) => <h3>{round.name}</h3>}
        renderMatch={(m) => (
          <div className="match-card">
            <div>{m.home}{m.homeScore ?? ""}</div>
            <div>{m.away}{m.awayScore ?? ""}</div>
          </div>
        )}
      />
    </div>
  )
}

Theming

bracketkit ships no visual styling beyond layout. Connectors inherit currentColor and expose two CSS variables; every part has a data-* hook:

[data-bracket-root] {
  --bracket-connector-color: #64748b;
  --bracket-connector-width: 2px;
}
[data-bracket-match] { /* your card wrapper */ }

Prefer shadcn/ui?

npx shadcn@latest add https://hrmasss.github.io/bracketkit/r/bracket.json

You get a styled, batteries-included <Bracket> using your shadcn tokens — and you own the code.

Try it

If you've fought the Safari foreignObject bug, I'd love to know whether this saves you the headache. Issues and PRs welcome.