Bevel

Redline

Proposed edits in proofreader's red ink, each with a sign-here flag to accept or reject it. Nothing is applied until someone decides.

A notice clause

Three suggested edits. Decide each one, or all at once.

9.2 Notice. Either party may end this lease on deleted: thirty (30) inserted: sixty (60) days' written notice delivered deleted: by hand inserted: by hand or by registered post to the address in Schedule Binserted: . Notice by email alone is not valid.

Installation

Add with the CLI

  1. Once per project, add the Bevel registry to components.json.

    components.json
    {
      "registries": {
        "@bevel": "https://bevel-ui.vercel.app/r/{name}.json"
      }
    }
  2. Add Redline. This brings in the Bevel foundation, installs cn, and merges the styles into your global CSS.

    Terminal
    npx shadcn@latest add @bevel/redline
Install manually
  1. Install the dependencies.

    Terminal
    pnpm add cn
  2. Add the Bevel foundation, if you haven't already. Light, depth, materials, motion and shape tokens, plus the utilities components use to apply them.

    styles/bevel.css
    /*
     * Bevel foundation. Every component builds on these tokens and utilities;
     * component stylesheets only add their own palettes and shapes.
     *
     * Layers:
     *   1. Light     one source, above and slightly in front of the screen.
     *   2. Depth     named elevations built from the light: recessed, flush,
     *                raised, pressed, object.
     *   3. Material  a surface recipe (leather, plastic, metal) that reads
     *                --bevel-material and --bevel-material-edge.
     *   4. Motion    two curves, a spring, and durations named by purpose.
     *   5. Shape     radii, stitching, and the type scale for amounts.
     */
    
    :root {
      /* 1. Light */
      --bevel-highlight: oklch(1 0 0 / 0.22);
      --bevel-highlight-strong: oklch(1 0 0 / 0.35);
      --bevel-shade: oklch(0 0 0 / 0.32);
      --bevel-shade-strong: oklch(0 0 0 / 0.5);
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
      /* Stamped into a surface: shaded on the upper inner edge, lit below. */
      --bevel-deboss: 0 -1px 0 oklch(0 0 0 / 0.45), 0 1px 0 oklch(1 0 0 / 0.12);
    
      /* 3. Material defaults; components and variants override these. */
      --bevel-material: oklch(0.47 0.085 52);
      --bevel-material-edge: oklch(0.3 0.06 44);
      --bevel-material-recess: oklch(0.2 0.008 260); /* neutral dark; materials set their own */
      --bevel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .7 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
      --bevel-grain-size: 180px;
    
      /* 2. Depth */
      --bevel-depth-recessed:
        inset 0 1px 2px var(--bevel-shade-strong),
        0 1px 0 var(--bevel-highlight);
      --bevel-depth-flush:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade);
      --bevel-depth-raised:
        inset 0 1px 0 var(--bevel-highlight),
        0 1px 0 var(--bevel-material-edge),
        0 2px 0 var(--bevel-material-edge),
        0 3px 5px var(--bevel-shade);
      --bevel-depth-pressed:
        inset 0 1px 2px var(--bevel-shade),
        0 1px 0 var(--bevel-highlight);
      /* A whole object resting on the page. */
      --bevel-depth-object:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade),
        0 0 0 1px var(--bevel-material-edge),
        0 1px 1px var(--bevel-cast),
        0 6px 12px -2px var(--bevel-cast),
        0 18px 32px -8px var(--bevel-cast);
      /* The shadow under a loose object, for filter: drop-shadow(). */
      --bevel-depth-cast-sm: 0 4px 6px var(--bevel-cast);
      /* A thin object (a card) lying on another surface. */
      --bevel-depth-sheet:
        inset 0 1px 0 var(--bevel-highlight-strong),
        inset 0 0 0 1px oklch(0 0 0 / 0.12),
        0 -1px 2px oklch(0 0 0 / 0.18);
    
      /* 4. Motion */
      --bevel-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
      --bevel-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
      /* Underdamped spring, ~8% overshoot. Pure CSS; pair with --bevel-duration-move or longer. */
      --bevel-ease-spring: linear(0, 0.06, 0.202, 0.38, 0.561, 0.723, 0.856, 0.955, 1.023, 1.062, 1.081, 1.083, 1.075, 1.062, 1.047, 1.032, 1.019, 1.009, 1.002, 0.997, 0.994, 0.993, 0.993, 0.994, 1);
      --bevel-duration-press: 100ms; /* a control giving under a finger */
      --bevel-duration-small: 200ms; /* hover lifts, tabs, small state changes */
      --bevel-duration-move: 300ms; /* objects moving: cards, drawers */
      --bevel-duration-ambient: 520ms; /* whole-scene changes: lighting */
      --bevel-stagger: 30ms;
      --bevel-lift: 3px; /* how far an object rises on hover */
      --bevel-perspective: 30rem; /* viewing distance for parts that turn in 3D (covers, flaps) */
      /* Unchosen items in a group sit back in shadow. */
      --bevel-dim: 0.82;
    
      /* 5. Shape */
      --bevel-radius-xs: 0.25rem;
      --bevel-radius-sm: 0.5rem;
      --bevel-radius-md: 0.625rem;
      --bevel-radius-lg: 1rem;
      --bevel-radius-xl: 1.5rem;
      --bevel-stitch-width: 1.5px;
      --bevel-stitch-inset: 0.375rem;
      /* Amounts: the figure, its secondary parts (currency, cents), labels, fine print. */
      --bevel-amount: 2.25rem;
      --bevel-amount-minor: 1.25rem;
      --bevel-label: 0.75rem;
      --bevel-fine: 0.6875rem;
      --bevel-tracking-label: 0.08em; /* stamped labels */
      --bevel-tracking-print: 0.06em; /* printed marks on cards */
    
      /* Paper and ink */
      --bevel-paper: oklch(0.975 0.008 85);
      --bevel-paper-foreground: oklch(0.28 0.02 260);
      --bevel-paper-muted: oklch(0.48 0.015 260);
      --bevel-paper-rule: oklch(0.78 0.04 240 / 0.45);
      --bevel-paper-margin: oklch(0.68 0.14 25 / 0.45);
      --bevel-fibre: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9 .35' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .18 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3C/svg%3E");
      /* Ink soaks unevenly: mostly solid with pale specks. Used as a mask. */
      --bevel-ink-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='i'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 0 0 0 2.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23i)'/%3E%3C/svg%3E");
      --bevel-ink-red: oklch(0.52 0.19 27);
      --bevel-ink-blue: oklch(0.44 0.14 262);
      --bevel-ink-green: oklch(0.5 0.12 155);
      --bevel-ink-black: oklch(0.3 0.01 260);
      --bevel-ink-amber: oklch(0.55 0.13 65);
    
      /* Brass and ivory: instrument hardware and dial faces */
      --bevel-brass: oklch(0.76 0.11 82);
      --bevel-brass-edge: oklch(0.52 0.08 70);
      --bevel-ivory: oklch(0.95 0.025 90);
      --bevel-ivory-foreground: oklch(0.25 0.015 60);
      --bevel-needle: oklch(0.55 0.2 27);
    
      /* Desk stationery: legal pads, pens, pencils, exhibit stickers */
      --bevel-pad: oklch(0.95 0.075 98); /* yellow legal pad */
      --bevel-pad-foreground: oklch(0.27 0.03 255);
      --bevel-pad-rule: oklch(0.72 0.06 235 / 0.45);
      --bevel-pad-margin: oklch(0.62 0.16 25 / 0.55);
      --bevel-pen-red: oklch(0.52 0.2 27); /* correcting pen */
      --bevel-pen-blue: oklch(0.42 0.15 262); /* writing pen */
      --bevel-pencil: oklch(0.55 0.01 260); /* not written in ink yet */
      --bevel-sticker: oklch(0.9 0.12 95); /* exhibit sticker */
      --bevel-sticker-foreground: oklch(0.26 0.03 70);
      /* Handwriting. Bring a marker font (e.g. Permanent Marker) for the full effect; these are fallbacks. */
      --bevel-font-marker: "Permanent Marker", "Marker Felt", "Chalkboard SE", ui-rounded, "Arial Rounded MT Bold", system-ui, sans-serif;
    }
    
    .dark {
      --bevel-highlight: oklch(1 0 0 / 0.14);
      /* Paper under dim light, not inverted: it's still paper. */
      --bevel-paper: oklch(0.88 0.008 85);
      --bevel-ivory: oklch(0.86 0.022 90);
      --bevel-pad: oklch(0.86 0.07 98);
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /* Depth */
    @utility bevel-recessed {
      background-color: var(--bevel-material-recess);
      box-shadow: var(--bevel-depth-recessed);
    }
    @utility bevel-flush {
      box-shadow: var(--bevel-depth-flush);
    }
    @utility bevel-raised {
      background-color: color-mix(in oklch, var(--bevel-material), white 7%);
      background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 60%);
      box-shadow: var(--bevel-depth-raised);
    }
    @utility bevel-pressed {
      background-color: color-mix(in oklch, var(--bevel-material), black 4%);
      background-image: none;
      box-shadow: var(--bevel-depth-pressed);
    }
    @utility bevel-object {
      box-shadow: var(--bevel-depth-object);
    }
    
    /* Materials */
    @utility bevel-leather {
      background-color: var(--bevel-material);
      background-image:
        var(--bevel-grain),
        linear-gradient(to bottom, var(--bevel-highlight), transparent 28%, transparent 72%, var(--bevel-shade));
      background-size: var(--bevel-grain-size) var(--bevel-grain-size), 100% 100%;
      background-blend-mode: soft-light, normal;
    }
    @utility bevel-plastic {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(115deg, var(--bevel-highlight-strong), transparent 40%),
        linear-gradient(to bottom, transparent 25%, oklch(0 0 0 / 0.45));
      box-shadow: var(--bevel-depth-sheet);
    }
    @utility bevel-metal {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade) 100%),
        repeating-linear-gradient(90deg, oklch(1 0 0 / 0.06) 0 1px, transparent 1px 3px);
      box-shadow:
        inset 0 0 0 1px oklch(0 0 0 / 0.25),
        inset 0 1px 0 var(--bevel-highlight-strong);
    }
    
    /* Details */
    @utility bevel-deboss {
      text-shadow: var(--bevel-deboss);
    }
    @utility bevel-stitch {
      border-color: var(--bevel-stitch, oklch(0.88 0.05 82 / 0.8));
      border-style: dashed;
      border-width: var(--bevel-stitch-width);
      filter: drop-shadow(0 1px 0 var(--bevel-shade));
    }
    /* A lit fill sitting in a recess (meters, progress). Colour from --bevel-fill. */
    @utility bevel-fill {
      background-color: var(--bevel-fill);
      background-image: linear-gradient(to bottom, oklch(1 0 0 / 0.55), transparent 55%, oklch(0 0 0 / 0.18));
      box-shadow: 0 0 6px -1px var(--bevel-fill);
    }
    /* A small indicator light. Colour from --bevel-led. */
    @utility bevel-led {
      background-color: var(--bevel-led);
      background-image: radial-gradient(circle at 35% 30%, oklch(1 0 0 / 0.85), transparent 45%);
      box-shadow:
        0 0 6px var(--bevel-led),
        inset 0 -1px 1px oklch(0 0 0 / 0.35),
        0 1px 0 var(--bevel-highlight);
    }
    
    /* Paper: a sheet resting on the surface, with fibre in it. */
    @utility bevel-paper {
      background-color: var(--bevel-paper);
      background-image: var(--bevel-fibre);
      background-size: 240px 240px;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
    /* Ink on paper: multiplies into the surface and soaks unevenly. Colour from --bevel-ink. */
    @utility bevel-ink {
      color: var(--bevel-ink, var(--bevel-ink-red));
      border-color: currentColor;
      mix-blend-mode: multiply;
      mask-image: var(--bevel-ink-texture);
      mask-size: 160px 160px;
    }
    /* Glass over an instrument window: one reflection from the light above. Put on an overlay element. */
    @utility bevel-glass {
      pointer-events: none;
      background-image: linear-gradient(165deg, var(--bevel-highlight-strong) 0%, transparent 38%, transparent 70%, var(--bevel-shade) 100%);
    }
    
    /* Material families: one per screen. Set on the screen's container. */
    [data-bevel-family="leather"] {
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
    }
    [data-bevel-family="paper"] {
      --bevel-cast: oklch(0.3 0.01 250 / 0.16);
    }
    .dark :is([data-bevel-family="leather"], [data-bevel-family="paper"]) {
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /*
     * Ruled paper: rules every --bevel-rule-height and a margin line at
     * --bevel-rule-margin. Set --bevel-sheet for the paper colour. The ruling
     * scrolls with the content.
     */
    @utility bevel-ruled {
      background-color: var(--bevel-sheet, var(--bevel-paper));
      background-image:
        linear-gradient(
          to right,
          transparent calc(var(--bevel-rule-margin) - 1px),
          var(--bevel-sheet-margin, var(--bevel-paper-margin)) calc(var(--bevel-rule-margin) - 1px) calc(var(--bevel-rule-margin) + 1px),
          transparent calc(var(--bevel-rule-margin) + 1px)
        ),
        repeating-linear-gradient(
          to bottom,
          transparent 0 calc(var(--bevel-rule-height) - 1px),
          var(--bevel-sheet-rule, var(--bevel-paper-rule)) calc(var(--bevel-rule-height) - 1px) var(--bevel-rule-height)
        ),
        var(--bevel-fibre);
      background-size: 100% 100%, 100% 100%, 240px 240px;
      background-attachment: local;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
  3. Copy redline.tsx into components/ui.

    components/redline.tsx
    "use client"
    
    import * as React from "react"
    import { cn } from "cn"
    
    type RedlineDecision = "accepted" | "rejected"
    
    type RedlineContextValue = {
      decisions: Record<string, RedlineDecision>
      decide: (id: string, decision: RedlineDecision | undefined) => void
      /** Decide several changes in one update. */
      decideMany: (updates: Record<string, RedlineDecision>) => void
      register: (id: string) => () => void
      ids: string[]
    }
    
    const RedlineContext = React.createContext<RedlineContextValue | null>(null)
    
    function useRedline() {
      const context = React.useContext(RedlineContext)
      if (!context) throw new Error("Redline parts must be inside <Redline>")
      return context
    }
    
    type RedlineProps = React.ComponentProps<"div"> & {
      decisions?: Record<string, RedlineDecision>
      defaultDecisions?: Record<string, RedlineDecision>
      onDecisionsChange?: (decisions: Record<string, RedlineDecision>) => void
    }
    
    /** A document with proposed edits. Nothing is applied until someone decides. */
    function Redline({
      className,
      decisions: decisionsProp,
      defaultDecisions = {},
      onDecisionsChange,
      ...props
    }: RedlineProps) {
      const [uncontrolled, setUncontrolled] = React.useState(defaultDecisions)
      const decisions = decisionsProp ?? uncontrolled
      const [ids, setIds] = React.useState<string[]>([])
      // Stable, so a decision doesn't make every change re-register.
      const register = React.useCallback((id: string) => {
        setIds((list) => (list.includes(id) ? list : [...list, id]))
        return () => setIds((list) => list.filter((x) => x !== id))
      }, [])
    
      const context = React.useMemo<RedlineContextValue>(
        () => ({
          decisions,
          ids,
          decide: (id, decision) => {
            const next = { ...decisions }
            if (decision) next[id] = decision
            else delete next[id]
            if (decisionsProp === undefined) setUncontrolled(next)
            onDecisionsChange?.(next)
          },
          decideMany: (updates) => {
            const next = { ...decisions, ...updates }
            if (decisionsProp === undefined) setUncontrolled(next)
            onDecisionsChange?.(next)
          },
          register,
        }),
        [decisions, ids, decisionsProp, onDecisionsChange, register]
      )
    
      return (
        <RedlineContext value={context}>
          <div
            data-slot="redline"
            className={cn("bevel-paper flex w-full max-w-xl flex-col rounded-(--bevel-radius-xs)", className)}
            {...props}
          />
        </RedlineContext>
      )
    }
    
    /** The text being edited. */
    function RedlineDocument({ className, ...props }: React.ComponentProps<"div">) {
      return (
        <div
          data-slot="redline-document"
          className={cn("p-5 text-base leading-loose text-(--bevel-paper-foreground)", className)}
          {...props}
        />
      )
    }
    
    type RedlineChangeProps = Omit<React.ComponentProps<"span">, "children"> & {
      /** Stable id for this change. */
      id: string
      /** Text taken out. */
      deletion?: string
      /** Text put in. */
      insertion?: string
    }
    
    /**
     * One proposed edit, marked in red ink with a flag to accept or reject it.
     * Accepted shows the new text; rejected keeps the original.
     */
    function RedlineChange({ id, deletion, insertion, className, ...props }: RedlineChangeProps) {
      const { decisions, decide, register } = useRedline()
      React.useEffect(() => register(id), [id, register])
      const decision = decisions[id]
    
      if (decision === "accepted") {
        return (
          <span data-slot="redline-change" data-decision="accepted" className={className} {...props}>
            {insertion}
          </span>
        )
      }
      if (decision === "rejected") {
        return (
          <span data-slot="redline-change" data-decision="rejected" className={className} {...props}>
            {deletion}
          </span>
        )
      }
    
      const summary = [deletion && `delete “${deletion}”`, insertion && `insert “${insertion}”`]
        .filter(Boolean)
        .join(", ")
    
      return (
        <span
          data-slot="redline-change"
          data-decision="pending"
          role="group"
          aria-label={`Proposed change: ${summary}`}
          className={cn("text-(--bevel-pen-red)", className)}
          {...props}
        >
          {deletion && (
            <del className="decoration-(--bevel-pen-red) decoration-(length:--redline-strike)">
              <span className="sr-only">deleted: </span>
              {deletion}
            </del>
          )}
          {deletion && insertion && " "}
          {insertion && (
            <ins className="underline decoration-(--bevel-pen-red) decoration-double underline-offset-4">
              <span className="sr-only">inserted: </span>
              {insertion}
            </ins>
          )}
          <span
            data-slot="redline-flag"
            className="redline-flag-shape mx-1 inline-flex translate-y-0.5 items-center gap-0.5 bg-(--redline-flag) py-0.5 pr-1 pl-2.5 align-middle text-(--redline-flag-foreground) shadow-(--bevel-depth-sheet)"
          >
            <FlagButton label="Accept change" onClick={() => decide(id, "accepted")}>
              <path d="M3 8.5l3 3 7-7.5" />
            </FlagButton>
            <FlagButton label="Reject change" onClick={() => decide(id, "rejected")}>
              <path d="M4 4l8 8M12 4l-8 8" />
            </FlagButton>
          </span>
        </span>
      )
    }
    
    function FlagButton({
      label,
      onClick,
      children,
    }: {
      label: string
      onClick: () => void
      children: React.ReactNode
    }) {
      return (
        <button
          type="button"
          aria-label={label}
          onClick={onClick}
          className="inline-grid size-6 cursor-pointer place-items-center rounded-(--bevel-radius-xs) outline-none hover:bg-(--redline-flag-foreground)/10 focus-visible:ring-2 focus-visible:ring-(--bevel-pen-blue)/60"
        >
          <svg viewBox="0 0 16 16" aria-hidden className="size-3.5" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            {children}
          </svg>
        </button>
      )
    }
    
    /** Count of open changes, with accept-all and reject-all. */
    function RedlineToolbar({ className, ...props }: React.ComponentProps<"div">) {
      const { ids, decisions, decideMany } = useRedline()
      const pending = ids.filter((id) => !decisions[id])
      const applyAll = (decision: RedlineDecision) =>
        decideMany(Object.fromEntries(pending.map((id) => [id, decision])))
      return (
        <div
          role="toolbar"
          aria-label="Changes"
          data-slot="redline-toolbar"
          className={cn(
            "flex items-center justify-between gap-3 border-t border-(--bevel-paper-rule) px-5 py-3 text-sm",
            className
          )}
          {...props}
        >
          <span aria-live="polite" className="text-(--bevel-paper-muted)">
            {pending.length === 0
              ? "All changes decided"
              : `${pending.length} change${pending.length === 1 ? "" : "s"} to review`}
          </span>
          <span className="flex gap-2">
            <ToolbarButton disabled={pending.length === 0} onClick={() => applyAll("rejected")}>
              Reject all
            </ToolbarButton>
            <ToolbarButton disabled={pending.length === 0} onClick={() => applyAll("accepted")}>
              Accept all
            </ToolbarButton>
          </span>
        </div>
      )
    }
    
    function ToolbarButton(props: React.ComponentProps<"button">) {
      return (
        <button
          type="button"
          className="h-8 rounded-(--bevel-radius-sm) border border-(--bevel-paper-foreground)/30 px-3 font-medium text-(--bevel-paper-foreground) outline-none hover:bg-(--bevel-paper-foreground)/5 focus-visible:ring-2 focus-visible:ring-(--bevel-pen-blue)/50 disabled:opacity-50"
          {...props}
        />
      )
    }
    
    export { Redline, RedlineDocument, RedlineChange, RedlineToolbar }
    export type { RedlineDecision, RedlineProps, RedlineChangeProps }
  4. Copy redline.css into your styles.

    styles/redline.css
    /*
     * Redline: proposed edits marked up in proofreader's red ink, each with a
     * "sign here" flag to accept or reject it. Builds on bevel.css (paper,
     * pens, sticker, motion).
     */
    
    :root {
      --redline-flag: var(--bevel-sticker);
      --redline-flag-foreground: var(--bevel-sticker-foreground);
      --redline-strike: 2px;
    }
    
    /* The flag is cut to a point on its leading edge, like a sign-here tab. */
    @utility redline-flag-shape {
      clip-path: polygon(0.5rem 0, 100% 0, 100% 100%, 0.5rem 100%, 0 50%);
    }
  5. Import the stylesheets after Tailwind in your global CSS, foundation first.

    globals.css
    @import "./bevel.css";
    @import "./redline.css";

Anatomy

Each part is its own component. Leave out the ones you don't need.

Anatomy
<Redline onDecisionsChange={save}>
  <RedlineDocument>
    … <RedlineChange id="period" deletion="thirty" insertion="sixty" /> …
  </RedlineDocument>
  <RedlineToolbar />
</Redline>

Parts

PartRendersWhat it does
RedlinedivHolds the decisions: decisions, defaultDecisions, onDecisionsChange ({ id: accepted | rejected }).
RedlineDocumentdivThe text being edited.
RedlineChangespan (group)One edit: id, deletion, insertion. Uses real del and ins, read as “deleted” and “inserted”. Its flag holds Accept and Reject.
RedlineToolbardiv (toolbar)How many changes are left, with Accept all and Reject all.