I’m building a React component that uses <svg> to display text with a gradient fill and stroke. On iOS (iPhone, iPad, Safari), the text is partially cut off near the top and bottom—almost like the bounding box or stroke area is getting clipped. However, on desktop browsers (Chrome, Brave, Firefox) and on Android devices, the text renders perfectly without any clipping.
Probably part of the problem is due to the italic font but, as you can see, accent marks like “~” are also being cut.
Edges and accent marks being cut
I work on Ubuntu and I can’t check it directly from a MacIOS system, but the text boundaries looks like this in Brave:
Text in Brave
The site is for a client and it’s still being developed, but I provided it for test in: https://museclub-teste.rerunsset.com/
I’ve tried several common fixes without success:
- Increasing the viewBox dimensions significantly (adding extra padding around the text).
- Inserting an invisible <rect> to expand the bounding box.
- Setting overflow=”visible” on both the <svg> and <text> elements.
- Changing dominantBaseline from “middle” to “central”, “hanging”, or other baselines.
- Removing or altering fontStyle=”italic” and adjusting line-height or leading-none.
Despite these attempts, the text still gets truncated only on iOS Safari. I expected the text to render fully (with no cutting on top or sides), just like it does on other platforms. Does anyone know how to reliably prevent <text> clipping in <svg> on iOS devices?
Versions:
FeaturedTextSVG.tsx:
import React, { ComponentPropsWithoutRef, ReactNode } from "react";
import { cn } from "@/utils/classMerge";
type GradientStop = {
offset: string;
stopColor: string;
};
type GradientData = {
fillID: string;
strokeID: string;
fillStops: GradientStop[];
strokeStops: GradientStop[];
};
type GradientMap = Record<'dark', GradientData>;
/**
* Map containing the fill and stroke gradient definitions
* for each theme variant.
*/
const GRADIENT_MAP: GradientMap = {
dark: {
fillID: "fillGradientDark",
strokeID: "strokeGradientDark",
fillStops: [
{ offset: "0%", stopColor: "#DB98A5" },
{ offset: "50%", stopColor: "#C96981" },
{ offset: "100%", stopColor: "#91314A" },
],
strokeStops: [
{ offset: "0%", stopColor: "#472B2D" },
{ offset: "5%", stopColor: "#7C4A4F" },
{ offset: "10%", stopColor: "#965A61" },
{ offset: "15%", stopColor: "#BC707B" },
{ offset: "51%", stopColor: "#EFBFC7" },
{ offset: "61%", stopColor: "#EFC6CA" },
{ offset: "69.5%", stopColor: "#D9959F" },
{ offset: "90%", stopColor: "#948184" },
{ offset: "100%", stopColor: "#615254" },
],
},
};
/**
* Props for FeaturedTextSVG component.
*/
type FeaturedTextSVGProps = {
children: ReactNode; // The text to display
width?: number | string;
height?: number | string;
fontSize?: number | string;
strokeWidth?: number;
className?: string;
textClassName?: string;
textX?: string;
} & Pick<ComponentPropsWithoutRef<'text'>, 'textAnchor' | 'fontStyle' | 'fontWeight'>;
/**
* Renders text with both fill and stroke gradients via SVG.
* The stroke precisely follows the shape of each letter.
*/
export function FeaturedTextSVG({
children,
width = 500,
height = 100,
strokeWidth = 3,
className,
textAnchor = 'middle',
fontStyle = 'italic',
fontWeight = '800',
textX = "50%"
}: FeaturedTextSVGProps): ReactNode {
// Retrieve gradient definitions from the map
const gradients = GRADIENT_MAP['dark']; // dark is default so far
return (
<svg
overflow={'visible'}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
style={{ display: "block" }}
className={cn("maw-w-[500px] px-2 lg:maw-w-[700px] max-lg:v", className)}
>
<defs>
{/* Fill gradient */}
<linearGradient
id={gradients.fillID}
gradientUnits="objectBoundingBox"
x1="0" y1="0"
x2="0" y2="1"
>
{gradients.fillStops.map((stop, idx) => (
<stop
key={idx}
offset={stop.offset}
stopColor={stop.stopColor}
/>
))}
</linearGradient>
{/* Stroke gradient */}
<linearGradient
id={gradients.strokeID}
gradientUnits="objectBoundingBox"
x1="0" y1="0"
x2="1" y2="0"
gradientTransform="rotate(100)"
>
{gradients.strokeStops.map((stop, idx) => (
<stop
key={idx}
offset={stop.offset}
stopColor={stop.stopColor}
/>
))}
</linearGradient>
</defs>
<text
x={textX}
y={parseFloat(height as string) / 2 + strokeWidth / 2}
dominantBaseline='central'
textAnchor={textAnchor}
fontWeight={fontWeight}
fontStyle={fontStyle}
fill={`url(#${gradients.fillID})`}
stroke={`url(#${gradients.strokeID})`}
strokeWidth={strokeWidth}
strokeLinejoin="round"
style={{ overflow: 'visible', paintOrder: "stroke" }}
strokeLinecap="round"
className={cn("uppercase pr-4 max-[350px]:text-5xl text-[3.25rem] leading-none md:text-6xl lg:text-8xl")}
>
{children}
</text>
</svg>
);
};
Usage:
<BigText
size={'mini'}
className="title-text-shadow text-center font-[600]"
>
Seja a mulher <br /> que você nasceu
</BigText>
<FeaturedTextSVG
width={600}
height={60}
fontSize={80}
strokeWidth={1.2}
textAnchor="middle"
className="title-drop-shadow mt-1 md:mt-4 lg:mt-8 text-[3.6rem]"
>
para ser!
</FeaturedTextSVG>
</div>