Retro Grid
Retro Grid is a React and Tailwind CSS effect component from Magic UI, licensed MIT. Copy it and paste it into your project.
Magic UI
MIT
effects
What it is
Retro Grid is a React and Tailwind CSS effect component from Magic UI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from Magic UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Retro Grid
"use client"
import { useEffect, useRef, useState } from "react"
import type { CSSProperties, HTMLAttributes } from "react"
import { cn } from "@/lib/utils"
const ANIMATION_DURATION_SECONDS = 15
const GRID_HEIGHT_RATIO = 3
const GRID_LINE_ALIGNMENT_OFFSET_PX = 0.5
const GRID_LINE_ANTIALIAS_MULTIPLIER = 0.9
const GRID_LINE_WIDTH_PX = 0.92
const GRID_START_OFFSET_RATIO = -0.5
const GRID_WIDTH_RATIO = 6
const GRID_X_OFFSET_RATIO = -2
const MAX_ANGLE = 89
const MAX_DEVICE_PIXEL_RATIO = 2
const MIN_ANGLE = 1
const PERSPECTIVE_PX = 200
const FALLBACK_ANIMATION_NAME = "retro-grid-fallback-scroll"
const FALLBACK_STYLES = `
@keyframes ${FALLBACK_ANIMATION_NAME} {
from {
transform: translateY(-50%);
}
to {
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
[data-retro-grid-scroll="true"] {
animation: none !important;
transform: translateY(-50%) !important;
}
}
`
const VERTEX_SHADER_SOURCE = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}
`
const FRAGMENT_SHADER_SOURCE = `
#extension GL_OES_standard_derivatives : enable
precision highp float;
uniform vec2 u_container_size;
uniform vec2 u_viewport_size;
uniform vec4 u_line_color;
uniform float u_angle;
uniform float u_cell_size;
uniform float u_device_pixel_ratio;
uniform float u_time;
const float animationDurationSeconds = ${ANIMATION_DURATION_SECONDS.toFixed(1)};
const float gridHeightRatio = ${GRID_HEIGHT_RATIO.toFixed(1)};
const float gridStartOffsetRatio = ${GRID_START_OFFSET_RATIO.toFixed(1)};
const float gridWidthRatio = ${GRID_WIDTH_RATIO.toFixed(1)};
const float gridXOffsetRatio = ${GRID_X_OFFSET_RATIO.toFixed(1)};
const float gridLineAlignmentOffsetPx = ${GRID_LINE_ALIGNMENT_OFFSET_PX.toFixed(1)};
const float gridLineAntialiasMultiplier = ${GRID_LINE_ANTIALIAS_MULTIPLIER.toFixed(1)};
const float horizontalLodLevelOneEndPx = 5.6;
const float horizontalLodLevelOneStartPx = 2.8;
const float horizontalLodLevelTwoEndPx = 3.0;
const float horizontalLodLevelTwoStartPx = 1.4;
const float horizontalCompressionEndPx = 2.8;
const float horizontalCompressionStartPx = 1.2;
const float lineWidthPx = ${GRID_LINE_WIDTH_PX.toFixed(2)};
const float perspectivePx = ${PERSPECTIVE_PX.toFixed(1)};
const float gridTravelRatio = 0.5;
const float verticalCompressionEndPx = 2.6;
const float verticalCompressionStartPx = 1.0;
const float verticalEdgeCompressionEnd = 0.95;
const float verticalEdgeCompressionStart = 0.45;
const float verticalLodLevelEnd = 0.64;
const float verticalLodLevelStart = 0.22;
const float verticalTopCompressionEndCells = 6.0;
const float verticalTopCompressionStartCells = 2.0;
float renderGridLine(
float wrappedCoord,
float antiAliasWidth,
float softnessBoost
) {
return 1.0 - smoothstep(
lineWidthPx,
lineWidthPx + (antiAliasWidth * (1.5 + softnessBoost)),
wrappedCoord
);
}
void main() {
float angle = radians(clamp(u_angle, 1.0, 89.0));
float sinAngle = sin(angle);
float cosAngle = cos(angle);
vec2 screen = vec2(
(gl_FragCoord.x / u_device_pixel_ratio) - (u_container_size.x * 0.5),
(u_container_size.y * 0.5) - (gl_FragCoord.y / u_device_pixel_ratio)
);
vec3 rayOrigin = vec3(0.0, 0.0, perspectivePx);
vec3 rayDirection = normalize(vec3(screen, -perspectivePx));
vec3 planeXAxis = vec3(1.0, 0.0, 0.0);
vec3 planeYAxis = vec3(0.0, cosAngle, sinAngle);
vec3 planeNormal = normalize(cross(planeXAxis, planeYAxis));
float denominator = dot(rayDirection, planeNormal);
if (abs(denominator) < 0.0001) {
discard;
}
float distanceToPlane = dot(-rayOrigin, planeNormal) / denominator;
if (distanceToPlane <= 0.0) {
discard;
}
vec3 hitPoint = rayOrigin + (rayDirection * distanceToPlane);
float localX = hitPoint.x;
float localY = dot(hitPoint, planeYAxis);
float gridWidth = u_viewport_size.x * gridWidthRatio;
float gridHeight = u_viewport_size.y * gridHeightRatio;
float gridScrollSpeed = (gridHeight * gridTravelRatio) / animationDurationSeconds;
float patternOffsetY = u_time * gridScrollSpeed;
float gridLeft = (-0.5 * u_container_size.x) + (gridXOffsetRatio * u_container_size.x);
float gridTop = (-0.5 * u_container_size.y) + (gridStartOffsetRatio * gridHeight);
vec2 planePosition = vec2(localX - gridLeft, localY - gridTop);
if (
planePosition.x < 0.0 ||
planePosition.y < 0.0 ||
planePosition.x > gridWidth ||
planePosition.y > gridHeight
) {
discard;
}
vec2 patternPosition = vec2(planePosition.x, planePosition.y - patternOffsetY);
vec2 wrapped = mod(
patternPosition + vec2(gridLineAlignmentOffsetPx),
u_cell_size
);
vec2 patternDerivative = max(fwidth(patternPosition), vec2(0.0001));
vec2 antiAliasWidth = patternDerivative * gridLineAntialiasMultiplier;
float horizontalCellSpanPx = u_cell_size / patternDerivative.y;
float horizontalCompression = 1.0 - smoothstep(
horizontalCompressionStartPx,
horizontalCompressionEndPx,
horizontalCellSpanPx
);
float verticalCellSpanPx = u_cell_size / patternDerivative.x;
float sideDistance = abs((planePosition.x / gridWidth) * 2.0 - 1.0);
float verticalEdgeCompression = smoothstep(
verticalEdgeCompressionStart,
verticalEdgeCompressionEnd,
sideDistance
);
float verticalTopCompression = 1.0 - smoothstep(
u_cell_size * verticalTopCompressionStartCells,
u_cell_size * verticalTopCompressionEndCells,
planePosition.y
);
float verticalCompression =
(1.0 - smoothstep(
verticalCompressionStartPx,
verticalCompressionEndPx,
verticalCellSpanPx
)) * verticalEdgeCompression * verticalTopCompression;
float horizontalSoftnessBoost = 1.0 + (horizontalCompression * 3.0);
float verticalSoftnessBoost = 1.0 + (verticalCompression * 3.5);
float verticalLod = smoothstep(
verticalLodLevelStart,
verticalLodLevelEnd,
verticalCompression
);
float verticalLineFine = renderGridLine(
wrapped.x,
antiAliasWidth.x,
verticalSoftnessBoost
);
float verticalWrappedLod = mod(
patternPosition.x + gridLineAlignmentOffsetPx,
u_cell_size * 2.0
);
float verticalLineCoarse = renderGridLine(
verticalWrappedLod,
antiAliasWidth.x,
verticalSoftnessBoost + verticalLod
);
float verticalLine = max(
verticalLineFine * (1.0 - verticalLod),
verticalLineCoarse * verticalLod
);
float horizontalLodLevelOne = 1.0 - smoothstep(
horizontalLodLevelOneStartPx,
horizontalLodLevelOneEndPx,
horizontalCellSpanPx
);
float horizontalLodLevelTwo = 1.0 - smoothstep(
horizontalLodLevelTwoStartPx,
horizontalLodLevelTwoEndPx,
horizontalCellSpanPx
);
float horizontalLineFine = renderGridLine(
wrapped.y,
antiAliasWidth.y,
horizontalSoftnessBoost
);
float horizontalWrappedLodOne = mod(
patternPosition.y + gridLineAlignmentOffsetPx,
u_cell_size * 2.0
);
float horizontalWrappedLodTwo = mod(
patternPosition.y + gridLineAlignmentOffsetPx,
u_cell_size * 4.0
);
float horizontalLineCoarse = renderGridLine(
horizontalWrappedLodOne,
antiAliasWidth.y,
horizontalSoftnessBoost + horizontalLodLevelOne
);
float horizontalLineExtraCoarse = renderGridLine(
horizontalWrappedLodTwo,
antiAliasWidth.y,
horizontalSoftnessBoost + horizontalLodLevelOne + horizontalLodLevelTwo
);
float horizontalLineReduced = max(
horizontalLineFine * (1.0 - horizontalLodLevelOne),
horizontalLineCoarse * horizontalLodLevelOne
);
float horizontalLine = max(
horizontalLineReduced * (1.0 - horizontalLodLevelTwo),
horizontalLineExtraCoarse * horizontalLodLevelTwo
);
float line = max(verticalLine, horizontalLine);
if (line <= 0.001) {
discard;
}
float alpha = u_line_color.a * line;
gl_FragColor = vec4(u_line_color.rgb * alpha, alpha);
}
`
interface RetroGridProps extends HTMLAttributes<HTMLDivElement> {
/**
* Additional CSS classes to apply to the grid container
*/
className?: string
/**
* Rotation angle of the grid in degrees
* @default 65
*/
angle?: number
/**
* Grid cell size in pixels
* @default 60
*/
cellSize?: number
/**
* Grid opacity value between 0 and 1
* @default 0.5
*/
opacity?: number
/**
* Grid line color in light mode
* @default "gray"
*/
lightLineColor?: string
/**
* Grid line color in dark mode
* @default "gray"
*/
darkLineColor?: string
}
interface ProgramInfo {
attributeLocation: number
program: WebGLProgram
uniforms: {
angle: WebGLUniformLocation
cellSize: WebGLUniformLocation
containerSize: WebGLUniformLocation
devicePixelRatio: WebGLUniformLocation
lineColor: WebGLUniformLocation
time: WebGLUniformLocation
viewportSize: WebGLUniformLocation
}
}
let colorResolveContext: CanvasRenderingContext2D | null | undefined
function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max)
}
function createShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type)
if (!shader) {
return null
}
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
return shader
}
gl.deleteShader(shader)
return null
}
function createProgram(gl: WebGLRenderingContext) {
const vertexShader = createShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER_SOURCE)
const fragmentShader = createShader(
gl,
gl.FRAGMENT_SHADER,
FRAGMENT_SHADER_SOURCE
)
if (!vertexShader || !fragmentShader) {
return null
}
const program = gl.createProgram()
if (!program) {
gl.deleteShader(vertexShader)
gl.deleteShader(fragmentShader)
return null
}
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)
gl.linkProgram(program)
gl.deleteShader(vertexShader)
gl.deleteShader(fragmentShader)
if (gl.getProgramParameter(program, gl.LINK_STATUS)) {
return program
}
gl.deleteProgram(program)
return null
}
function getProgramInfo(
gl: WebGLRenderingContext,
program: WebGLProgram
): ProgramInfo | null {
const attributeLocation = gl.getAttribLocation(program, "a_position")
const angle = gl.getUniformLocation(program, "u_angle")
const cellSize = gl.getUniformLocation(program, "u_cell_size")
const containerSize = gl.getUniformLocation(program, "u_container_size")
const devicePixelRatio = gl.getUniformLocation(
program,
"u_device_pixel_ratio"
)
const lineColor = gl.getUniformLocation(program, "u_line_color")
const time = gl.getUniformLocation(program, "u_time")
const viewportSize = gl.getUniformLocation(program, "u_viewport_size")
if (
attributeLocation < 0 ||
!angle ||
!cellSize ||
!containerSize ||
!devicePixelRatio ||
!lineColor ||
!time ||
!viewportSize
) {
return null
}
return {
attributeLocation,
program,
uniforms: {
angle,
cellSize,
containerSize,
devicePixelRatio,
lineColor,
time,
viewportSize,
},
}
}
function isDarkMode(colorScheme: MediaQueryList) {
const root = document.documentElement
if (root.classList.contains("dark")) {
return true
}
if (root.classList.contains("light")) {
return false
}
return colorScheme.matches
}
function getColorResolveContext() {
if (colorResolveContext !== undefined) {
return colorResolveContext
}
const canvas = document.createElement("canvas")
canvas.width = 1
canvas.height = 1
colorResolveContext = canvas.getContext("2d", {
willReadFrequently: true,
})
return colorResolveContext
}
function resolveLineColor(color: string, element: HTMLElement) {
const resolver = document.createElement("span")
resolver.style.color = color
resolver.style.opacity = "0"
resolver.style.pointerEvents = "none"
resolver.style.position = "absolute"
element.appendChild(resolver)
const resolvedColor = getComputedStyle(resolver).color
resolver.remove()
const context = getColorResolveContext()
if (!context) {
return new Float32Array([0.5, 0.5, 0.5, 1])
}
context.clearRect(0, 0, 1, 1)
context.fillStyle = resolvedColor
context.fillRect(0, 0, 1, 1)
const pixel = context.getImageData(0, 0, 1, 1).data
return new Float32Array([
pixel[0] / 255,
pixel[1] / 255,
pixel[2] / 255,
pixel[3] / 255,
])
}
function createFallbackGridStyle(
cellSize: number,
lineColor: string
): CSSProperties {
return {
animation: `${FALLBACK_ANIMATION_NAME} ${ANIMATION_DURATION_SECONDS}s linear infinite`,
backgroundImage: `linear-gradient(to right, ${lineColor} 1px, transparent 0), linear-gradient(to bottom, ${lineColor} 1px, transparent 0)`,
backgroundRepeat: "repeat",
backgroundSize: `${cellSize}px ${cellSize}px`,
transform: "translateY(-50%)",
}
}
export function RetroGrid({
className,
angle = 65,
cellSize = 60,
opacity = 0.5,
lightLineColor = "gray",
darkLineColor = "gray",
style,
...props
}: RetroGridProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [isWebGlReady, setIsWebGlReady] = useState(false)
const angleRef = useRef(angle)
const cellSizeRef = useRef(cellSize)
const darkLineColorRef = useRef(darkLineColor)
const lightLineColorRef = useRef(lightLineColor)
const syncSceneRef = useRef<(() => void) | null>(null)
useEffect(() => {
angleRef.current = angle
cellSizeRef.current = cellSize
darkLineColorRef.current = darkLineColor
lightLineColorRef.current = lightLineColor
syncSceneRef.current?.()
}, [angle, cellSize, darkLineColor, lightLineColor])
useEffect(() => {
const canvas = canvasRef.current
const container = containerRef.current
if (!canvas || !container) {
return
}
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)")
const colorScheme = window.matchMedia("(prefers-color-scheme: dark)")
let animationFrameId: number | null = null
let currentWidth = 0
let currentHeight = 0
let currentDevicePixelRatio = 1
let gl: WebGLRenderingContext | null = null
let isVisible = true
let isContextLost = false
let lineColor = resolveLineColor(lightLineColorRef.current, container)
let positionBuffer: WebGLBuffer | null = null
let programInfo: ProgramInfo | null = null
const getContext = () => {
const nextGl = canvas.getContext("webgl", {
alpha: true,
antialias: true,
premultipliedAlpha: true,
})
if (!nextGl || !nextGl.getExtension("OES_standard_derivatives")) {
return null
}
return nextGl
}
const releasePipeline = (shouldDeleteResources: boolean) => {
if (shouldDeleteResources && gl) {
if (positionBuffer) {
gl.deleteBuffer(positionBuffer)
}
if (programInfo) {
gl.deleteProgram(programInfo.program)
}
}
positionBuffer = null
programInfo = null
if (shouldDeleteResources) {
gl = null
}
}
const initializePipeline = () => {
const nextGl = getContext()
if (!nextGl) {
releasePipeline(false)
return false
}
gl = nextGl
releasePipeline(true)
gl = nextGl
const program = createProgram(nextGl)
if (!program) {
return false
}
const nextProgramInfo = getProgramInfo(nextGl, program)
if (!nextProgramInfo) {
nextGl.deleteProgram(program)
return false
}
const nextPositionBuffer = nextGl.createBuffer()
if (!nextPositionBuffer) {
nextGl.deleteProgram(program)
return false
}
nextGl.bindBuffer(nextGl.ARRAY_BUFFER, nextPositionBuffer)
nextGl.bufferData(
nextGl.ARRAY_BUFFER,
new Float32Array([-1, -1, 3, -1, -1, 3]),
nextGl.STATIC_DRAW
)
positionBuffer = nextPositionBuffer
programInfo = nextProgramInfo
return true
}
const updateLineColor = () => {
const activeColor = isDarkMode(colorScheme)
? darkLineColorRef.current
: lightLineColorRef.current
lineColor = resolveLineColor(activeColor, container)
}
const resizeCanvas = () => {
currentWidth = Math.floor(container.clientWidth)
currentHeight = Math.floor(container.clientHeight)
if (currentWidth === 0 || currentHeight === 0 || !gl) {
return
}
currentDevicePixelRatio = Math.min(
window.devicePixelRatio || 1,
MAX_DEVICE_PIXEL_RATIO
)
canvas.width = Math.floor(currentWidth * currentDevicePixelRatio)
canvas.height = Math.floor(currentHeight * currentDevicePixelRatio)
canvas.style.width = `${currentWidth}px`
canvas.style.height = `${currentHeight}px`
gl.viewport(0, 0, canvas.width, canvas.height)
}
const draw = (timestamp: number) => {
if (
currentWidth === 0 ||
currentHeight === 0 ||
!gl ||
!positionBuffer ||
!programInfo ||
isContextLost
) {
return
}
gl.useProgram(programInfo.program)
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
gl.enableVertexAttribArray(programInfo.attributeLocation)
gl.vertexAttribPointer(
programInfo.attributeLocation,
2,
gl.FLOAT,
false,
0,
0
)
gl.clearColor(0, 0, 0, 0)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.uniform1f(
programInfo.uniforms.angle,
clamp(angleRef.current, MIN_ANGLE, MAX_ANGLE)
)
gl.uniform1f(
programInfo.uniforms.cellSize,
Math.max(cellSizeRef.current, 1)
)
gl.uniform2f(
programInfo.uniforms.containerSize,
currentWidth,
currentHeight
)
gl.uniform1f(
programInfo.uniforms.devicePixelRatio,
currentDevicePixelRatio
)
gl.uniform4fv(programInfo.uniforms.lineColor, lineColor)
gl.uniform1f(
programInfo.uniforms.time,
reducedMotion.matches ? 0 : timestamp / 1000
)
gl.uniform2f(
programInfo.uniforms.viewportSize,
window.innerWidth,
window.innerHeight
)
gl.drawArrays(gl.TRIANGLES, 0, 3)
}
const stopAnimation = () => {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId)
animationFrameId = null
}
}
const frame = (timestamp: number) => {
draw(timestamp)
if (!reducedMotion.matches && isVisible) {
animationFrameId = requestAnimationFrame(frame)
return
}
animationFrameId = null
}
const syncScene = () => {
if (isContextLost) {
stopAnimation()
setIsWebGlReady(false)
return
}
if (!gl || !positionBuffer || !programInfo) {
if (!initializePipeline()) {
stopAnimation()
setIsWebGlReady(false)
return
}
}
resizeCanvas()
if (currentWidth === 0 || currentHeight === 0) {
stopAnimation()
return
}
updateLineColor()
draw(performance.now())
setIsWebGlReady(true)
if (reducedMotion.matches || !isVisible) {
stopAnimation()
return
}
if (animationFrameId === null) {
animationFrameId = requestAnimationFrame(frame)
}
}
syncSceneRef.current = syncScene
const resizeObserver = new ResizeObserver(() => {
syncScene()
})
resizeObserver.observe(container)
const handleWindowResize = () => {
syncScene()
}
const intersectionObserver = new IntersectionObserver(([entry]) => {
isVisible = entry?.isIntersecting ?? false
if (isVisible) {
syncScene()
return
}
stopAnimation()
})
intersectionObserver.observe(container)
const themeObserver = new MutationObserver(() => {
syncScene()
})
themeObserver.observe(document.documentElement, {
attributeFilter: ["class"],
attributes: true,
})
const handleMotionChange = () => {
syncScene()
}
const handleColorSchemeChange = () => {
syncScene()
}
const handleContextLost = (event: Event) => {
event.preventDefault()
isContextLost = true
stopAnimation()
releasePipeline(false)
setIsWebGlReady(false)
}
const handleContextRestored = () => {
isContextLost = false
syncScene()
}
reducedMotion.addEventListener("change", handleMotionChange)
colorScheme.addEventListener("change", handleColorSchemeChange)
window.addEventListener("resize", handleWindowResize)
canvas.addEventListener("webglcontextlost", handleContextLost)
canvas.addEventListener("webglcontextrestored", handleContextRestored)
syncScene()
return () => {
stopAnimation()
resizeObserver.disconnect()
intersectionObserver.disconnect()
themeObserver.disconnect()
reducedMotion.removeEventListener("change", handleMotionChange)
colorScheme.removeEventListener("change", handleColorSchemeChange)
window.removeEventListener("resize", handleWindowResize)
canvas.removeEventListener("webglcontextlost", handleContextLost)
canvas.removeEventListener("webglcontextrestored", handleContextRestored)
syncSceneRef.current = null
releasePipeline(!isContextLost)
}
}, [])
const gridStyles = {
...style,
opacity,
} as CSSProperties
const normalizedAngle = clamp(angle, MIN_ANGLE, MAX_ANGLE)
const normalizedCellSize = Math.max(cellSize, 1)
const fallbackProjectionStyles = {
perspective: `${PERSPECTIVE_PX}px`,
} as CSSProperties
const fallbackRotationStyles = {
transform: `rotateX(${normalizedAngle}deg)`,
} as CSSProperties
const lightFallbackGridStyles = createFallbackGridStyle(
normalizedCellSize,
lightLineColor
)
const darkFallbackGridStyles = createFallbackGridStyle(
normalizedCellSize,
darkLineColor
)
return (
<div
ref={containerRef}
className={cn(
"pointer-events-none absolute size-full overflow-hidden",
className
)}
style={gridStyles}
{...props}
>
<style>{FALLBACK_STYLES}</style>
{!isWebGlReady ? (
<div className="absolute inset-0" style={fallbackProjectionStyles}>
<div className="absolute inset-0" style={fallbackRotationStyles}>
<div
data-retro-grid-scroll="true"
className="absolute inset-[0%_0px] ml-[-200%] h-[300vh] w-[600vw] origin-[100%_0_0] dark:hidden"
style={lightFallbackGridStyles}
/>
<div
data-retro-grid-scroll="true"
className="absolute inset-[0%_0px] ml-[-200%] hidden h-[300vh] w-[600vw] origin-[100%_0_0] dark:block"
style={darkFallbackGridStyles}
/>
</div>
</div>
) : null}
<canvas
ref={canvasRef}
className={cn(
"absolute inset-0 size-full",
isWebGlReady ? "opacity-100" : "opacity-0"
)}
/>
<div className="absolute inset-0 bg-linear-to-t from-white to-transparent to-90% dark:from-black" />
</div>
)
}