Format string processing similar to date or printf()
Return the number of bytes written to buf
You will notice that functions like sprintf() and strftime() don't return a pointer, but rather an integer that says something about the number of bytes that (would) have been written to the output buffer. This is much more useful than just copying the pointer to buf, which doesn't give the caller any new information.
Where is the string returned by the callback function allocated?
The callback function returns a pointer to a string. But where is this allocated? Your format() function doesn't call free(), so either the string should be stored in some statically allocated array, or it is allocated on the heap. In the former case, unless you return a pointer to a string literal, your format() function can only be used from one thread at a time. If you return memory that is allocated on the heap, then you have to keep track of it so the caller can clean up all the allocated memory once format() returns.
Consider having the callback function write into buf directly
To solve the above issue, and to avoid an unncessary copy, you can pass a pointer into the buffer and the remaining size to the callback function, and have the callback function write directly to the buffer. For example:
char*
format(const char* format, char *buf, size_t len, size_t (*cb)(char c, void* ctx, char *buf, size_t len), void *ctx) {
    ...
        if (*curr == '%' &amp;&amp; *next) 
        {
            if (*next == '%') // escaped %, copy it over and skip
            {
                buf[i++] = *format++;
                continue;
            }
            i += cb(*next, ctx, buf + i, len - i - 1);
            ++format;
            continue;
        }
    ...
}

And then your callback function can look like:
size_t example_cb(char c, void *ctx, char *buf, size_t len) {
    if (c == 'f') {
        if (len &gt; 3)
            len = 3;
        memcpy(buf, &quot;FOO&quot;, len);
        return len;
    }

    return 0;
}

You can create a helper function to avoid repeating the above construction, and to safely write any string to the buffer:
size_t emplace_string(const char *str, char *buf, size_t max_len) {
    size_t len = strlen(str);
    if (len &gt; max_len)
        len = max_len;
    memcpy(buf, str, len);
    return len;
}
        
size_t example_cb(char c, void *ctx, char *buf, size_t len) {
    switch (c) {
    case 'f':
        return emplace_string(&quot;FOO&quot;, buf, len);
    case 'B':
        return emplace_string(&quot;bar&quot;, buf, len);
    ...
    default:
        return 0;
    } 
}