A Razor UI component (RichTextBase) for Sandbox that renders inline rich text with optional icons, images, styles and many CSS animations. It exposes many properties to control color, font, borders, shadows, gradients, animations and builds style state in ParseRichText.
@namespace Sandbox
@using Sandbox;
@using Sandbox.UI;
@inherits Panel
@implements IRichTextPanel
<root class="richtext">
@* @if ( Stats.Length > 0 && Player.Local is Player localPlayer )
{
<RazorTooltip>
<Content class="richtext">
<label>@Text</label>
@if ( !string.IsNullOrEmpty( Input ) )
{
<Image [email protected](Input, InputGlyphSize.Small, false) />
}
else if ( !string.IsNullOrEmpty( ImagePath ) )
{
<img src="@ImagePath" />
}
@if ( ShowIcon && !string.IsNullOrEmpty( Icon ) )
{
<i>@Icon</i>
}
</Content>
</RazorTooltip>
}
else
{ *@
@if ( PrependIcon )
{
@if ( !string.IsNullOrEmpty( Input ) )
{
<Image [email protected](Input, InputGlyphSize.Small, false) />
}
else if ( !string.IsNullOrEmpty( ImagePath ) )
{
var imgSize = ImageSize ?? 20f;
<img src="@ImagePath" style="width: @(imgSize)px; height: @(imgSize)px;" />
}
@if ( ShowIcon && !string.IsNullOrEmpty( Icon ) )
{
<i>@Icon</i>
}
}
@if ( !HideText )
{
<label>@Text</label>
}
@if ( !PrependIcon )
{
@if ( !string.IsNullOrEmpty( Input ) )
{
<Image [email protected](Input, InputGlyphSize.Small, false) />
}
else if ( !string.IsNullOrEmpty( ImagePath ) )
{
var imgSize = ImageSize ?? 20f;
<img src="@ImagePath" style="width: @(imgSize)px; height: @(imgSize)px;" />
}
@if ( ShowIcon && !string.IsNullOrEmpty( Icon ) )
{
<i>@Icon</i>
}
}
@* } *@
</root>
<style>
.richtext {
align-items: center;
gap: 2px;
color: white;
overflow: visible;
label {
overflow: visible;
}
}
img, Image {
width: 16px;
height: 16px;
}
i {
position: relative;
top: 2px;
}
@@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
@@keyframes wiggle {
0%, 100% { transform: rotate(-2deg); }
50% { transform: rotate(2deg); }
}
@@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@@keyframes hop {
0% { transform: translateY(-4px); }
100% { transform: translateY(1px); }
}
@@keyframes jump {
0% { transform: translateY(0) scaleX(1); }
1% { transform: translateY(-3px) scaleX(1); }
4% { transform: translateY(-7px) scaleX(1); }
8% { transform: translateY(-9px) scaleX(1); }
12% { transform: translateY(-7px) scaleX(1); }
16% { transform: translateY(-2px) scaleX(1); }
19% { transform: translateY(0) scaleX(1.1); }
21%, 100% { transform: translateY(0) scaleX(1); }
}
@@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
@@keyframes glow {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.3); }
}
@@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
@@keyframes turn {
0% { transform: rotate(-8deg); }
100% { transform: rotate(8deg); }
}
@@keyframes heartbeat {
0% { transform: scale(1); }
25% { transform: scale(1.1); }
50% { transform: scale(1); }
75% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@@keyframes tada {
0%, 100% { transform: scale(1) rotate(0deg); }
10%, 20% { transform: scale(0.9) rotate(-3deg); }
30%, 50%, 70%, 90% { transform: scale(1.1) rotate(3deg); }
40%, 60%, 80% { transform: scale(1.1) rotate(-3deg); }
}
@@keyframes rainbow {
0% { filter: hue-rotate(0deg); }
100% { filter: hue-rotate(360deg); }
}
@@keyframes god {
0%, { transform: scale(1); }
100% { transform: scale(1.05); }
}
@@keyframes stretch-y {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(1.2); }
}
@@keyframes recoil {
0% { transform: translateX(0) scale(1); }
15% { transform: translateX(-6px) scale(0.95); }
30% { transform: translateX(1px) scale(1.02); }
45% { transform: translateX(0) scale(1); }
100% { transform: translateX(0) scale(1); }
}
@@keyframes wave-up {
0%, 100% { transform: translateY(5px); }
50% { transform: translateY(-5px); }
}
@@keyframes wave-down {
0%, 100% { transform: translateY(-5px); }
50% { transform: translateY(5px); }
}
@@keyframes jitter {
0% { transform: translate(0, 0); }
10% { transform: translate(-1px, 1px); }
20% { transform: translate(1px, -1px); }
30% { transform: translate(-1px, -1px); }
40% { transform: translate(1px, 1px); }
50% { transform: translate(-1px, 0); }
60% { transform: translate(1px, 0); }
70% { transform: translate(0, -1px); }
80% { transform: translate(0, 1px); }
90% { transform: translate(-1px, 1px); }
100% { transform: translate(0, 0); }
}
@@keyframes throb {
0%, 100% { transform: scale(0.95); opacity: 0.4; }
50% { transform: scale(1.05); opacity: 1; }
}
@@keyframes curse {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
10% { transform: translate(-0.5px, -0.5px) rotate(-0.5deg) scale(1.02); }
20% { transform: translate(0.5px, 0.5px) rotate(0.5deg) scale(0.98); }
30% { transform: translate(-0.5px, 0.5px) rotate(-0.3deg) scale(1.01); }
40% { transform: translate(0.5px, -0.5px) rotate(0.3deg) scale(0.99); }
50% { transform: translate(-0.3px, -0.3px) rotate(-0.2deg) scale(1.02); }
60% { transform: translate(0.3px, 0.3px) rotate(0.2deg) scale(0.98); }
70% { transform: translate(-0.5px, 0.3px) rotate(-0.4deg) scale(1.01); }
80% { transform: translate(0.5px, -0.3px) rotate(0.4deg) scale(0.99); }
90% { transform: translate(-0.3px, 0.5px) rotate(-0.1deg) scale(1); }
100% { transform: translate(0, 0) rotate(0deg) scale(1); }
}
@@keyframes track {
0% { transform: translate(0, 0) rotate(0deg); }
15% { transform: translate(2px, -1.5px) rotate(3deg); }
16% { transform: translate(2px, -1.5px) rotate(3deg); }
35% { transform: translate(-1.5px, 1px) rotate(-4deg); }
36% { transform: translate(-1.5px, 1px) rotate(-4deg); }
55% { transform: translate(0.5px, -2px) rotate(2deg); }
56% { transform: translate(0.5px, -2px) rotate(2deg); }
75% { transform: translate(1px, 1.5px) rotate(-2deg); }
76% { transform: translate(1px, 1.5px) rotate(-2deg); }
95% { transform: translate(-0.5px, -0.5px) rotate(1deg); }
100% { transform: translate(0, 0) rotate(0deg); }
}
@@keyframes echo-clone {
0% { transform: translateX(0); opacity: 1; }
10% { transform: translateX(2px); opacity: 0.5; }
15% { transform: translateX(4px); opacity: 0.25; }
20% { transform: translateX(6px); opacity: 0; }
25% { transform: translateX(0); opacity: 0; }
100% { transform: translateX(0); opacity: 1; }
}
@@keyframes slide-return {
0% { transform: translateX(-12px); }
100% { transform: translateX(12px); }
}
@@keyframes wobble {
0% { transform: rotate(0deg); }
20% { transform: rotate(-8deg); }
21% { transform: rotate(-8deg); }
40% { transform: rotate(12deg); }
41% { transform: rotate(12deg); }
60% { transform: rotate(-5deg); }
61% { transform: rotate(-5deg); }
80% { transform: rotate(6deg); }
81% { transform: rotate(6deg); }
100% { transform: rotate(0deg); }
}
@@keyframes steer {
0% { transform: rotate(-12deg); }
100% { transform: rotate(12deg); }
}
@@keyframes zoom {
0% { transform: scale(1.3); }
100% { transform: scale(0.7); }
}
@@keyframes oscillate {
0% { transform: scale(0.85); }
100% { transform: scale(1.15); }
}
@@keyframes dvd {
0% { transform: translate(-3px, -2px); }
20% { transform: translate(0px, 1px); }
40% { transform: translate(2px, -3px); }
60% { transform: translate(-1px, 2px); }
80% { transform: translate(-3px, -3px); }
100% { transform: translate(0px, 2px); }
}
</style>
@code
{
public virtual string Icon => "";
public virtual string Input => "";
public virtual string ImagePath => "";
public virtual string TextOverride => null;
public virtual Color Color => Color.Transparent;
public virtual bool HideText => false;
public virtual bool PrependIcon => false;
public virtual RichTextStatEntry[] Stats => [];
public virtual int? FontWeight => null;
public virtual float? FontSize => null;
public virtual bool Italic => false;
public virtual bool Underline => false;
public virtual Sandbox.UI.TextDecorationStyle? UnderlineStyle => null;
public virtual Color? UnderlineColor => null;
public virtual float? UnderlineWidth => null;
public virtual float? TextShadowBlur => null;
public virtual float? TextShadowOffsetY => null;
public virtual Color? TextShadowColor => null;
public virtual Color? BackgroundColor => null;
public virtual float? BorderRadius => null;
public virtual float? BorderWidth => null;
public virtual Color? BorderColor => null;
public virtual string BorderStyle => null;
public virtual float? SkewX => null;
public virtual float? SkewY => null;
public virtual float? TextStrokeWidth => null;
public virtual Color? TextStrokeColor => null;
public virtual Color? BackgroundGradientStart => null;
public virtual Color? BackgroundGradientEnd => null;
public virtual float? BackgroundGradientAngle => null;
public virtual string AnimationName => null;
public virtual float? AnimationDuration => null;
public virtual string AnimationTimingFunction => null;
/// <summary>
/// normal, reverse, alternate, alternate-reverse
/// </summary>
public virtual string AnimationDirection => null;
public virtual float? MarginLeft => null;
public virtual float? MarginRight => null;
public virtual float? MarginTop => null;
public virtual float? MarginBottom => null;
public float? ImageSize { get; set; }
// "normal" - plays forward(0% → 100%), then jumps back to 0% and repeats
// "reverse" - plays backward(100% → 0%), then jumps to 100% and repeats
// "alternate" - plays forward then backward(0% → 100% → 0%), creating a smooth loop
// "alternate-reverse" - plays backward then forward(100% → 0% → 100%)
bool ShowIcon = true;
string Text;
public void ParseRichText(string text)
{
Text = !string.IsNullOrEmpty(TextOverride) ? TextOverride : text;
if(Color != Color.Transparent)
{
Style.FontColor = Color;
}
if(FontWeight.HasValue)
Style.FontWeight = FontWeight.Value;
if(FontSize.HasValue)
Style.FontSize = Length.Pixels(FontSize.Value);
if(Italic)
Style.FontStyle = Sandbox.UI.FontStyle.Italic;
if(Underline)
Style.TextDecorationLine = Sandbox.UI.TextDecoration.Underline;
if(UnderlineStyle.HasValue)
Style.TextDecorationStyle = UnderlineStyle.Value;
if(UnderlineColor.HasValue)
Style.TextDecorationColor = UnderlineColor.Value;
if(UnderlineWidth.HasValue)
Style.Set("text-decoration-thickness", $"{UnderlineWidth.Value}px");
if(TextShadowBlur.HasValue && TextShadowOffsetY.HasValue && TextShadowColor.HasValue)
{
Style.TextShadow.Clear();
Style.TextShadow.Add(new Shadow
{
OffsetX = 0,
OffsetY = TextShadowOffsetY.Value,
Blur = TextShadowBlur.Value,
Color = TextShadowColor.Value
});
}
if(BackgroundColor.HasValue)
Style.BackgroundColor = BackgroundColor.Value;
if(BorderRadius.HasValue)
{
var radius = Length.Pixels(BorderRadius.Value);
Style.BorderTopLeftRadius = radius;
Style.BorderTopRightRadius = radius;
Style.BorderBottomRightRadius = radius;
Style.BorderBottomLeftRadius = radius;
}
if(BorderWidth.HasValue)
{
var width = Length.Pixels(BorderWidth.Value);
Style.BorderTopWidth = width;
Style.BorderRightWidth = width;
Style.BorderBottomWidth = width;
Style.BorderLeftWidth = width;
}
if(BorderColor.HasValue)
{
Style.BorderTopColor = BorderColor.Value;
Style.BorderRightColor = BorderColor.Value;
Style.BorderBottomColor = BorderColor.Value;
Style.BorderLeftColor = BorderColor.Value;
}
if(!string.IsNullOrEmpty(BorderStyle))
{
Style.Set("border-style", BorderStyle);
}
if(SkewX.HasValue || SkewY.HasValue)
{
var transform = new PanelTransform();
transform.AddSkew(SkewX ?? 0, SkewY ?? 0, 0);
Style.Transform = transform;
}
if(TextStrokeWidth.HasValue)
Style.TextStrokeWidth = Length.Pixels(TextStrokeWidth.Value);
if(TextStrokeColor.HasValue)
Style.TextStrokeColor = TextStrokeColor.Value;
if(MarginLeft.HasValue)
Style.MarginLeft = Length.Pixels(MarginLeft.Value);
if(MarginRight.HasValue)
Style.MarginRight = Length.Pixels(MarginRight.Value);
if(MarginTop.HasValue)
Style.MarginTop = Length.Pixels(MarginTop.Value);
if(MarginBottom.HasValue)
Style.MarginBottom = Length.Pixels(MarginBottom.Value);
if(BackgroundGradientStart.HasValue && BackgroundGradientEnd.HasValue)
{
var angle = BackgroundGradientAngle ?? 90f;
Style.Set("background-image", $"linear-gradient({angle}deg, {BackgroundGradientStart.Value.Hex}, {BackgroundGradientEnd.Value.Hex})");
}
if(BackgroundColor.HasValue || (BackgroundGradientStart.HasValue && BackgroundGradientEnd.HasValue))
{
Style.PaddingLeft = Length.Pixels(3);
Style.PaddingRight = Length.Pixels(3);
}
if(!string.IsNullOrEmpty(AnimationName))
{
Style.AnimationName = AnimationName;
Style.AnimationIterationCount = float.PositiveInfinity;
if(AnimationDuration.HasValue)
Style.AnimationDuration = AnimationDuration.Value;
if(!string.IsNullOrEmpty(AnimationTimingFunction))
Style.AnimationTimingFunction = AnimationTimingFunction;
if(!string.IsNullOrEmpty(AnimationDirection))
Style.AnimationDirection = AnimationDirection;
}
if(HideText)
{
Style.PaddingLeft = Length.Pixels(3);
Style.PaddingRight = Length.Pixels(3);
}
if(!string.IsNullOrEmpty(ImagePath))
{
Style.PaddingLeft = Length.Pixels(0);
Style.PaddingRight = Length.Pixels(0);
}
}
protected override int BuildHash()
{
return System.HashCode.Combine( Text, Icon, ImagePath );
}
}