diff --git a/modules/badge/badge.go b/modules/badge/badge.go
index fdf9866f607a5..d2e9bd9d1bcc6 100644
--- a/modules/badge/badge.go
+++ b/modules/badge/badge.go
@@ -5,6 +5,7 @@ package badge
 
 import (
 	"strings"
+	"sync"
 	"unicode"
 
 	actions_model "code.gitea.io/gitea/models/actions"
@@ -49,23 +50,40 @@ func (b Badge) Width() int {
 	return b.Label.width + b.Message.width
 }
 
+// Style follows https://shields.io/badges
+const (
+	StyleFlat       = "flat"
+	StyleFlatSquare = "flat-square"
+)
+
 const (
 	defaultOffset     = 10
 	defaultFontSize   = 11
 	DefaultColor      = "#9f9f9f" // Grey
 	DefaultFontFamily = "DejaVu Sans,Verdana,Geneva,sans-serif"
+	DefaultStyle      = StyleFlat
 )
 
-var StatusColorMap = map[actions_model.Status]string{
-	actions_model.StatusSuccess:   "#4c1",    // Green
-	actions_model.StatusSkipped:   "#dfb317", // Yellow
-	actions_model.StatusUnknown:   "#97ca00", // Light Green
-	actions_model.StatusFailure:   "#e05d44", // Red
-	actions_model.StatusCancelled: "#fe7d37", // Orange
-	actions_model.StatusWaiting:   "#dfb317", // Yellow
-	actions_model.StatusRunning:   "#dfb317", // Yellow
-	actions_model.StatusBlocked:   "#dfb317", // Yellow
-}
+var GlobalVars = sync.OnceValue(func() (ret struct {
+	StatusColorMap       map[actions_model.Status]string
+	DejaVuGlyphWidthData map[rune]uint8
+	AllStyles            []string
+},
+) {
+	ret.StatusColorMap = map[actions_model.Status]string{
+		actions_model.StatusSuccess:   "#4c1",    // Green
+		actions_model.StatusSkipped:   "#dfb317", // Yellow
+		actions_model.StatusUnknown:   "#97ca00", // Light Green
+		actions_model.StatusFailure:   "#e05d44", // Red
+		actions_model.StatusCancelled: "#fe7d37", // Orange
+		actions_model.StatusWaiting:   "#dfb317", // Yellow
+		actions_model.StatusRunning:   "#dfb317", // Yellow
+		actions_model.StatusBlocked:   "#dfb317", // Yellow
+	}
+	ret.DejaVuGlyphWidthData = dejaVuGlyphWidthDataFunc()
+	ret.AllStyles = []string{StyleFlat, StyleFlatSquare}
+	return ret
+})
 
 // GenerateBadge generates badge with given template
 func GenerateBadge(label, message, color string) Badge {
@@ -93,7 +111,7 @@ func GenerateBadge(label, message, color string) Badge {
 
 func calculateTextWidth(text string) int {
 	width := 0
-	widthData := DejaVuGlyphWidthData()
+	widthData := GlobalVars().DejaVuGlyphWidthData
 	for _, char := range strings.TrimSpace(text) {
 		charWidth, ok := widthData[char]
 		if !ok {
diff --git a/modules/badge/badge_glyph_width.go b/modules/badge/badge_glyph_width.go
index e8e43ec9cb950..0d950c5a70e7f 100644
--- a/modules/badge/badge_glyph_width.go
+++ b/modules/badge/badge_glyph_width.go
@@ -3,8 +3,6 @@
 
 package badge
 
-import "sync"
-
 // DejaVuGlyphWidthData is generated by `sfnt.Face.GlyphAdvance(nil, <rune>, 11, font.HintingNone)` with DejaVu Sans
 // v2.37 (https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_2_37/dejavu-sans-ttf-2.37.zip).
 //
@@ -13,7 +11,7 @@ import "sync"
 //
 // A devtest page "/devtest/badge-actions-svg" could be used to check the rendered images.
 
-var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 {
+func dejaVuGlyphWidthDataFunc() map[rune]uint8 {
 	return map[rune]uint8{
 		32:  3,
 		33:  4,
@@ -205,4 +203,4 @@ var DejaVuGlyphWidthData = sync.OnceValue(func() map[rune]uint8 {
 		254: 7,
 		255: 7,
 	}
-})
+}
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
index 063ff42409fa9..995f9694264d1 100644
--- a/routers/web/devtest/devtest.go
+++ b/routers/web/devtest/devtest.go
@@ -4,6 +4,7 @@
 package devtest
 
 import (
+	"fmt"
 	"html/template"
 	"net/http"
 	"path"
@@ -128,6 +129,7 @@ func prepareMockDataBadgeCommitSign(ctx *context.Context) {
 func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
 	fontFamilyNames := strings.Split(badge.DefaultFontFamily, ",")
 	selectedFontFamilyName := ctx.FormString("font", fontFamilyNames[0])
+	selectedStyle := ctx.FormString("style", badge.DefaultStyle)
 	var badges []badge.Badge
 	badges = append(badges, badge.GenerateBadge("啊啊啊啊啊啊啊啊啊啊啊啊", "🌞🌞🌞🌞🌞", "green"))
 	for r := rune(0); r < 256; r++ {
@@ -141,7 +143,16 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
 	for i, b := range badges {
 		b.IDPrefix = "devtest-" + strconv.FormatInt(int64(i), 10) + "-"
 		b.FontFamily = selectedFontFamilyName
-		h, err := ctx.RenderToHTML("shared/actions/runner_badge", map[string]any{"Badge": b})
+		var h template.HTML
+		var err error
+		switch selectedStyle {
+		case badge.StyleFlat:
+			h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat", map[string]any{"Badge": b})
+		case badge.StyleFlatSquare:
+			h, err = ctx.RenderToHTML("shared/actions/runner_badge_flat-square", map[string]any{"Badge": b})
+		default:
+			err = fmt.Errorf("unknown badge style: %s", selectedStyle)
+		}
 		if err != nil {
 			ctx.ServerError("RenderToHTML", err)
 			return
@@ -151,6 +162,8 @@ func prepareMockDataBadgeActionsSvg(ctx *context.Context) {
 	ctx.Data["BadgeSVGs"] = badgeSVGs
 	ctx.Data["BadgeFontFamilyNames"] = fontFamilyNames
 	ctx.Data["SelectedFontFamilyName"] = selectedFontFamilyName
+	ctx.Data["BadgeStyles"] = badge.GlobalVars().AllStyles
+	ctx.Data["SelectedStyle"] = selectedStyle
 }
 
 func prepareMockData(ctx *context.Context) {
diff --git a/routers/web/repo/actions/badge.go b/routers/web/repo/actions/badge.go
index e920ecaf58063..d268a8df8af37 100644
--- a/routers/web/repo/actions/badge.go
+++ b/routers/web/repo/actions/badge.go
@@ -5,35 +5,38 @@ package actions
 
 import (
 	"errors"
-	"fmt"
 	"net/http"
 	"path/filepath"
 	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
 	"code.gitea.io/gitea/modules/badge"
+	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/services/context"
 )
 
 func GetWorkflowBadge(ctx *context.Context) {
 	workflowFile := ctx.PathParam("workflow_name")
-	branch := ctx.Req.URL.Query().Get("branch")
-	if branch == "" {
-		branch = ctx.Repo.Repository.DefaultBranch
-	}
-	branchRef := fmt.Sprintf("refs/heads/%s", branch)
-	event := ctx.Req.URL.Query().Get("event")
+	branch := ctx.FormString("branch", ctx.Repo.Repository.DefaultBranch)
+	event := ctx.FormString("event")
+	style := ctx.FormString("style")
 
-	badge, err := getWorkflowBadge(ctx, workflowFile, branchRef, event)
+	branchRef := git.RefNameFromBranch(branch)
+	b, err := getWorkflowBadge(ctx, workflowFile, branchRef.String(), event)
 	if err != nil {
 		ctx.ServerError("GetWorkflowBadge", err)
 		return
 	}
 
-	ctx.Data["Badge"] = badge
+	ctx.Data["Badge"] = b
 	ctx.RespHeader().Set("Content-Type", "image/svg+xml")
-	ctx.HTML(http.StatusOK, "shared/actions/runner_badge")
+	switch style {
+	case badge.StyleFlatSquare:
+		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat-square")
+	default: // defaults to badge.StyleFlat
+		ctx.HTML(http.StatusOK, "shared/actions/runner_badge_flat")
+	}
 }
 
 func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event string) (badge.Badge, error) {
@@ -48,7 +51,7 @@ func getWorkflowBadge(ctx *context.Context, workflowFile, branchName, event stri
 		return badge.Badge{}, err
 	}
 
-	color, ok := badge.StatusColorMap[run.Status]
+	color, ok := badge.GlobalVars().StatusColorMap[run.Status]
 	if !ok {
 		return badge.GenerateBadge(workflowName, "unknown status", badge.DefaultColor), nil
 	}
diff --git a/templates/devtest/badge-actions-svg.tmpl b/templates/devtest/badge-actions-svg.tmpl
index 8125793bb3fa4..5be4fb313128d 100644
--- a/templates/devtest/badge-actions-svg.tmpl
+++ b/templates/devtest/badge-actions-svg.tmpl
@@ -3,9 +3,16 @@
 	<div>
 		<h1>Actions SVG</h1>
 		<form class="tw-my-3">
-			{{range $fontName := .BadgeFontFamilyNames}}
-				<label><input name="font" type="radio" value="{{$fontName}}" {{Iif (eq $.SelectedFontFamilyName $fontName) "checked"}}>{{$fontName}}</label>
-			{{end}}
+			<div class="tw-mb-2">
+				{{range $fontName := .BadgeFontFamilyNames}}
+					<label><input name="font" type="radio" value="{{$fontName}}" {{Iif (eq $.SelectedFontFamilyName $fontName) "checked"}}>{{$fontName}}</label>
+				{{end}}
+			</div>
+			<div class="tw-mb-2">
+				{{range $style := .BadgeStyles}}
+					<label><input name="style" type="radio" value="{{$style}}" {{Iif (eq $.SelectedStyle $style) "checked"}}>{{$style}}</label>
+				{{end}}
+			</div>
 			<button>submit</button>
 		</form>
 		<div class="flex-text-block tw-flex-wrap">
diff --git a/templates/shared/actions/runner_badge_flat-square.tmpl b/templates/shared/actions/runner_badge_flat-square.tmpl
new file mode 100644
index 0000000000000..cc1dc1e8f3a90
--- /dev/null
+++ b/templates/shared/actions/runner_badge_flat-square.tmpl
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{{.Badge.Width}}" height="20"
+	role="img" aria-label="{{.Badge.Label.Text}}: {{.Badge.Message.Text}}">
+	<title>{{.Badge.Label.Text}}: {{.Badge.Message.Text}}</title>
+	<g shape-rendering="crispEdges">
+		<rect width="{{.Badge.Label.Width}}" height="20" fill="#555" />
+		<rect x="{{.Badge.Label.Width}}" width="{{.Badge.Message.Width}}" height="20" fill="{{.Badge.Color}}" />
+	</g>
+	<g fill="#fff" text-anchor="middle" font-family="{{.Badge.FontFamily}}"
+		text-rendering="geometricPrecision" font-size="{{.Badge.FontSize}}">
+		<text x="{{.Badge.Label.X}}" y="140"
+			transform="scale(.1)" fill="#fff" textLength="{{.Badge.Label.TextLength}}">{{.Badge.Label.Text}}</text>
+		<text x="{{.Badge.Message.X}}" y="140" transform="scale(.1)" fill="#fff"
+			textLength="{{.Badge.Message.TextLength}}">{{.Badge.Message.Text}}</text>
+	</g>
+</svg>
diff --git a/templates/shared/actions/runner_badge.tmpl b/templates/shared/actions/runner_badge_flat.tmpl
similarity index 100%
rename from templates/shared/actions/runner_badge.tmpl
rename to templates/shared/actions/runner_badge_flat.tmpl