Recursive AI

Read CSS animation keyframes for an element

All onehtml recipes

Find the @keyframes rule that wins for a name, and get the keyframes of each animation on an element the way Chrome’s getKeyframes() reports them, with var() substituted and the element’s own values filled in.

From 1.4.0, the cascade collects @keyframes rules. The library runs no clock and interpolates nothing: it gives a renderer or a test the keyframes, and the timing is up to you.

The page shows a saved-settings message, toast.html, with two animations:

CSS
@keyframes slide-in {
  from { opacity: 0; margin-inline-start: var(--offset) }
  60% { opacity: 1 }
}
@keyframes pulse {
  50% { background-color: #f4c95d }
}
.toast {
  --offset: -2rem;
  background-color: #fffaf0;
  animation: slide-in 300ms ease-out, pulse 1s 300ms 2;
}
C#
using RecursiveAI.OneHtml;
using RecursiveAI.OneHtml.Css;
 
Document doc = Document.Parse(File.ReadAllBytes("toast.html"));
DocumentStyles styles = DocumentStyles.Create(doc);
 
// The rule as written: one block per selector, with its declarations.
KeyframesRule rule = styles.GetKeyframesRule("slide-in")!;
Console.WriteLine($"@keyframes {rule.Name}: {rule.Blocks.Count} blocks, {rule.Origin}");
foreach (KeyframeBlock block in rule.Blocks)
{
    var declarations = block.Declarations.Select(d => $"{d.Property}: {d.Value}");
    Console.WriteLine($"  {block.Offset:P0}: {string.Join("; ", declarations)}");
}
 
// Each animation on the element, as Chrome's getKeyframes() reports it.
ComputedStyle toast = styles.GetStyle(doc.Root.QuerySelector(".toast"));
foreach (ResolvedKeyframes? animation in styles.GetAnimationKeyframes(toast))
{
    if (animation is null) continue; // `none`, or a name with no @keyframes rule
    string animated = string.Join(", ", animation.AnimatedProperties);
    Console.WriteLine($"{animation.Name} ({animation.TimingFunction}): {animated}");
    foreach (ResolvedKeyframe frame in animation.Keyframes)
    {
        var values = frame.Values.Select(v => $"{v.Property}: {v.Value}");
        string mark = frame.IsImplicit ? " (implicit)" : "";
        string head = $"{frame.Offset:P0}{mark} {frame.TimingFunction}";
        Console.WriteLine($"  {head}: {string.Join("; ", values)}");
    }
}
Text
@keyframes slide-in: 2 blocks, Author
  0%: opacity: 0; margin-inline-start: var(--offset)
  60%: opacity: 1
slide-in (ease-out): margin-left, opacity
  0% ease-out: margin-left: -2rem; opacity: 0
  60% ease-out: opacity: 1
  100% (implicit) ease-out: margin-left: 0; opacity: 1
pulse (ease): background-color
  0% (implicit) ease: background-color: rgb(255, 250, 240)
  50% ease: background-color: #f4c95d
  100% (implicit) ease: background-color: rgb(255, 250, 240)

How to read it:

  • GetKeyframesRule(name) returns the winning rule for a name: the unprefixed one over @-webkit-keyframes, then the later origin, the later layer, then the last in the document. KeyframesRules lists all of them. Blocks are as written: one per selector, with from as 0 and to as 1.
  • GetAnimationKeyframes(style) returns one entry per name in animation-name, in order: null for none or a name with no rule. It’s computed once per style object, so reading it again allocates nothing. ResolveKeyframes(style, name, index) resolves one animation.
  • var(--offset) was replaced from the element’s custom properties. margin-inline-start became margin-left, because the page is left to right.
  • The 100% keyframe of slide-in and both ends of pulse were added (IsImplicit) with the element’s own value of each property the first or last keyframe leaves out. Like Chrome, they take the animation’s own timing function.
  • Explicit values are the specified text. Implicit ones carry the computed value, which is why the background reads rgb(255, 250, 240). Chrome reports computed values everywhere: 20px for 1em, rgb(255, 0, 0) for red.

After a sheet edit, RestyleResult.ChangedKeyframes names the rules that changed. Elements that use them keep their computed style, so read GetAnimationKeyframes again on the new styles. The full rules are in @keyframes.