Git Workflow for Frontend Teams: Branch Strategy, PR Templates
Branch strategy, PR templates, and commit conventions that actually work for frontend teams shipping React and Tailwind components without stepping on each other.
Why Most Frontend Git Workflows Break Down
Honestly, the default "just commit to main" approach works fine until you have two developers touching the same Tailwind config on the same afternoon. Then it falls apart instantly.
Frontend codebases have a specific problem that backend repos don't: visual components are tightly coupled. A button component change ripples into a form, which ripples into a page layout. One merge conflict in globals.css can block three PRs at once.
The fix isn't some abstract branching philosophy. It's a concrete set of rules your team agrees on and actually follows. This article covers what's worked across real teams shipping React and Tailwind — including some conventions we use on Empire UI itself.
We're not talking about a bloated GitFlow with hotfix branches and release trains. That's overkill for most frontend teams. You need something lighter.
Branch Naming That Makes Your CI Happy
Branch names are CI configuration. Pick a pattern and encode it in your pipeline from day one. Most teams land on type/short-description and then forget to enforce it, which is how you end up with branches named johns-fix and URGENT in production.
The types that actually matter for frontend work: feat/, fix/, refactor/, chore/, docs/. That's it. You don't need hotfix/ unless you're running multiple release streams, and most frontend teams aren't.
Keep the description lowercase and hyphenated. No ticket numbers embedded unless your CI auto-links them — otherwise you're just adding noise. feat/theme-toggle-dark-mode tells you everything. feat/EMP-1234-theme-toggle-dark-mode-implementation-v2 tells you nothing extra and breaks tab completion.
In your GitHub Actions or GitLab CI, you can enforce this with a branch protection rule that matches a regex like ^(feat|fix|refactor|chore|docs)\/[a-z0-9-]+$. Takes five minutes to set up, saves hours of confusion.
The Trunk-Based Approach for Small Frontend Teams
If your team is under six developers, trunk-based development is almost always the right call. Everyone branches off main, opens a PR, gets it reviewed, and merges. No long-lived feature branches sitting around for two weeks accumulating conflicts.
The rule is simple: branches should live for no longer than two days. If a feature is too big to ship in two days, it's too big. Break it into smaller pieces — a component, a hook, a utility function. Ship those incrementally behind a feature flag if you need to.
Feature flags are where this gets interesting for component libraries. You can merge an unfinished <GlassCard /> component to main behind a flag, keep iterating on it, and your teammates aren't blocked waiting for you to finish. The code is in the repo, the flag keeps it off in production.
This pairs well with how we think about visual styling too. When you're building something like a glassmorphism generator, you want to ship the underlying CSS utilities first, then layer the UI on top in a separate PR.
PR Templates That Actually Get Filled In
Most PR templates fail because they ask for too much. A five-section template with checkboxes and a "motivation" field gets skipped every single time. Developers aren't being lazy — they're just optimizing for shipping.
Here's a template that's short enough to actually use. Drop this in .github/pull_request_template.md:
## What changed
<!-- One or two sentences. What does this PR do? -->
## Screenshots / video
<!-- Required for any visual change. Drag an image here. -->
## How to test
<!-- Steps a reviewer can follow to confirm this works -->
## Notes
<!-- Anything weird? Anything the reviewer should know first? -->Four sections. The screenshots field is the one that matters most for frontend work. If you're changing a component's spacing by 4px or tweaking rgba(255,255,255,0.15) on a glass effect, a before/after screenshot makes the review instant. Without it, the reviewer has to check out your branch locally, which means they'll procrastinate and your PR sits open for two days.
Commit Message Conventions That Don't Slow You Down
Conventional Commits is the right format. type(scope): description. It's not magic — it's just a standard that tools like Changesets, semantic-release, and GitHub's auto-changelog can parse without configuration.
For a component library or frontend app, your scopes should map to actual parts of the codebase. feat(button): add loading spinner variant is useful. feat(ui): stuff is not. Spend thirty seconds agreeing on scope names for your major areas — components, hooks, config, docs — and write them down in your CONTRIBUTING.md.
The part teams argue about: should you squash commits before merging? For feature PRs, yes. Your branch might have ten wip: trying stuff commits that mean nothing to the next developer reading git log. Squash them into one or two meaningful commits. For longer refactors, squashing into logical chunks (one per conceptual change) is better than a single 800-line commit.
Set up a commit lint step in your CI if you want to enforce this without relying on memory. @commitlint/cli with @commitlint/config-conventional takes about fifteen minutes to wire in and immediately stops "Fixed it" commits from reaching main.
Handling Tailwind Config Conflicts
Tailwind config is a merge conflict magnet. Two developers both extend the theme in the same week — one adds a custom shadow scale, one adds brand colors — and suddenly tailwind.config.ts is in conflict state. This is one of the most annoying things about working in Tailwind v4.0.2 era projects where the config has moved partly into CSS.
The solution is ownership. Assign one person on the team as the "design token owner" for the sprint. All config changes go through them, or at minimum get reviewed by them before merging. It's not bureaucracy — it's preventing your box shadow utilities from conflicting with a custom shadow the other developer just added under the same key name.
For the CSS layer additions in Tailwind v4, consider keeping custom utilities in a separate file (styles/utilities.css) that gets imported into your main styles. It doesn't eliminate conflicts but it narrows where they happen. When you're adding things like custom gradient utilities, isolating them makes review easier too.
And please — put tailwind.config.ts in your list of files that require two approvals before merging. One line in your CODEOWNERS file: tailwind.config.ts @your-team/design-system. Done.
Code Review for Visual Components: What to Actually Check
Reviewing frontend PRs is harder than reviewing backend PRs because half the important things aren't in the diff. A component can look perfect in the code and be completely broken at 320px viewport width.
What should reviewers actually check? First, does the component work with real content? Reviewers should mentally substitute the placeholder text for something three times longer. Does the layout still hold? Second, are the interactive states complete? Not just :hover — does :focus-visible work for keyboard navigation? Is there an active state?
For anything touching animation or transitions, ask for a screen recording instead of a screenshot. A 5-second Loom clip showing the interaction is worth more than a paragraph of description. This sounds like overhead but it's actually faster — the reviewer doesn't have to pull the branch locally.
Finally, who's responsible for catching visual regressions? If nobody owns it, nobody does it. Even a simple Chromatic or Percy setup catching diff on your component stories is better than relying on humans to catch every pixel. And if you're working with something visually complex like a theme toggle implementation, automated visual regression tests save significant debugging time.
Protecting Main and Setting Up Branch Policies
Main should require a passing CI check and at least one approval to merge. That's the minimum. No direct pushes, ever — even for the repo owner. The rule has to be unconditional or it gets violated the first time someone's in a hurry.
Beyond that, stale branch cleanup matters more than most teams realize. Branches that merged six months ago are still sitting in the remote, and every developer's local git branch -a output becomes unreadable. Set a GitHub Actions workflow to delete merged branches automatically, or just do a cleanup pass every sprint.
Here's a GitHub Actions snippet that deletes branches automatically after merge:
name: Delete merged branches
on:
pull_request:
types: [closed]
jobs:
cleanup:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Delete branch
uses: actions/github-script@v7
with:
script: |
github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${context.payload.pull_request.head.ref}`
})Wire it in, forget about it. Branch hygiene handled. Does your team really need to manage this manually? No. Automate the boring stuff and spend review time on things that actually matter.
FAQ
One approval is enough for most teams under ten developers. Two approvals for anything touching shared config files like tailwind.config.ts, global CSS, or design tokens. Requiring two approvals for every PR just creates bottlenecks without meaningfully improving quality.
Squash merge for feature PRs — it keeps main history clean and readable. Rebase merge if the PR has multiple genuinely distinct commits you want preserved. Avoid regular merge commits; they create noisy graphs without adding information. Pick one strategy per repo and configure GitHub or GitLab to enforce it.
Rebase it against main, resolve conflicts section by section, and get it merged or killed within 24 hours. Don't let it sit another week. If the PR is too large to merge after rebasing, split it — merge what's done, open a new PR for the remainder. Long-lived branches compound conflicts exponentially.
Run commitlint as a local commit-msg git hook via Husky, not in CI. Catching it locally gives instant feedback before the push. CI checks feel punitive and slow the loop. Add the Husky setup to your repo init script so new team members get it automatically on npm install.
Same trunk-based strategy, but scope your branch names more carefully. feat(ui)/button-ghost-variant for library changes, feat(app)/checkout-page for app changes. In your CI, trigger different test suites based on which paths changed. Tools like Turborepo or Nx handle this well — they only run affected package tests.
Branch off main, prefix with fix/, write the minimal change, get a quick review, merge. If you're on a release branch model, cherry-pick the commit to the release branch after. For most frontend teams shipping to production continuously, there's no release branch — fix goes to main, gets deployed in the next pipeline run, done.