Developers
February 3, 2026

On-Chain SVG NFTs on Flow

and
On-Chain SVG NFTs on Flow

Most NFTs store their artwork off-chain, on IPFS, Arweave, or centralized servers. The metadata might point to ipfs://Qm... or https://api.example.com/token/1, leaving the actual image hosted elsewhere. This works, but it introduces dependencies. IPFS gateways go down. Centralized servers get shut off. Your "permanent" NFT suddenly has a broken image.

On-chain SVG NFTs solve this by storing the artwork directly in the smart contract. The SVG markup lives on the blockchain forever, rendered by any browser that reads the token's metadata. No external dependencies. No broken links. True permanence.

The catch? On most chains, storing even a small SVG on-chain costs a fortune in gas. A 10KB SVG could easily run you $50-100+ on Ethereum mainnet. This is why on-chain NFT projects like Nouns, Loot, and Autoglyphs are the exception, not the rule, and why they typically use highly compressed or generative art.

Flow changes the economics. With gas costs roughly 1000x more cost efficient than Ethereum mainnet, storing a 10KB SVG costs fractions of a cent. This opens up on-chain NFTs to projects that would be economically impossible elsewhere.

Why SVGs?

SVGs are ideal for on-chain storage for several reasons:

  1. Text-based: SVGs are XML, which means they're human-readable text. No binary blobs.
  2. Compact: A well-crafted SVG can represent complex artwork in just a few kilobytes.
  3. Scalable: Vector graphics look crisp at any resolution, perfect for NFTs that might be displayed as thumbnails or billboards.
  4. Composable: SVG elements can be generated or combined programmatically on-chain.
  5. Native browser support: Every modern browser renders SVGs natively. No special viewers needed.

The Architecture

An on-chain SVG NFT returns a data: URI from its tokenURI function. Instead of pointing to an external URL, the metadata itself is embedded as a base64-encoded JSON blob, which in turn contains a base64-encoded SVG.

Here's what the flow looks like:

tokenURI(1) → data:application/json;base64,eyJuYW1lIjoi...
                                              ↓ (decoded)
                              {"name": "Token #1", "image": "data:image/svg+xml;base64,PHN2Zy..."}
                                                                                        ↓ (decoded)
                                                                        <svg>...</svg>

Marketplaces and wallets call tokenURI, decode the JSON, extract the image field, and render the SVG directly. No HTTP requests to external servers.

A Simple Implementation

Here's a minimal on-chain SVG NFT contract:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract OnChainSVGNFT is ERC721 {
    using Strings for uint256;

    uint256 private _tokenIdCounter;

    // Store SVG data for each token
    mapping(uint256 => string) private _tokenSVGs;

    constructor() ERC721("OnChainSVG", "OCSVG") {}

    function mint(string memory svgData) public returns (uint256) {
        uint256 tokenId = _tokenIdCounter++;
        _safeMint(msg.sender, tokenId);
        _tokenSVGs[tokenId] = svgData;
        return tokenId;
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_ownerOf(tokenId) != address(0), "Token does not exist");

        string memory svg = _tokenSVGs[tokenId];

        // Encode the SVG as base64
        string memory svgBase64 = Base64.encode(bytes(svg));

        // Build the JSON metadata
        string memory json = string(abi.encodePacked(
            '{"name": "OnChain SVG #', tokenId.toString(), '",',
            '"description": "A fully on-chain SVG NFT on Flow",',
            '"image": "data:image/svg+xml;base64,', svgBase64, '"}'
        ));

        // Encode the JSON as base64 and return as data URI
        return string(abi.encodePacked(
            "data:application/json;base64,",
            Base64.encode(bytes(json))
        ));
    }
}

To mint, you'd call mint() with raw SVG markup:

mint('<svg xmlns="<http://www.w3.org/2000/svg>" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="#00EF8B"/></svg>')

Generative On-Chain Art

The real power comes from generating SVGs programmatically. Instead of storing complete SVGs, you store parameters and construct the artwork on-chain:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Base64.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract GenerativeSVGNFT is ERC721 {
    using Strings for uint256;

    uint256 private _tokenIdCounter;

    // Store seed for each token - the art is generated from this
    mapping(uint256 => uint256) private _seeds;

    string[] private colors = ["#00EF8B", "#00D1FF", "#FF5733", "#9B59B6", "#F1C40F"];

    constructor() ERC721("GenerativeSVG", "GSVG") {}

    function mint() public returns (uint256) {
        uint256 tokenId = _tokenIdCounter++;
        _safeMint(msg.sender, tokenId);
        // Use block data and sender for pseudorandom seed
        _seeds[tokenId] = uint256(keccak256(abi.encodePacked(
            block.timestamp,
            block.prevrandao,
            msg.sender,
            tokenId
        )));
        return tokenId;
    }

    function generateSVG(uint256 tokenId) public view returns (string memory) {
        uint256 seed = _seeds[tokenId];

        // Generate deterministic "random" values from seed
        uint256 numShapes = (seed % 5) + 3; // 3-7 shapes

        string memory shapes = "";
        for (uint256 i = 0; i < numShapes; i++) {
            uint256 shapeSeed = uint256(keccak256(abi.encodePacked(seed, i)));

            uint256 cx = (shapeSeed % 80) + 10;
            uint256 cy = ((shapeSeed >> 8) % 80) + 10;
            uint256 r = ((shapeSeed >> 16) % 20) + 5;
            string memory color = colors[(shapeSeed >> 24) % colors.length];

            shapes = string(abi.encodePacked(
                shapes,
                '<circle cx="', cx.toString(),
                '" cy="', cy.toString(),
                '" r="', r.toString(),
                '" fill="', color,
                '" opacity="0.7"/>'
            ));
        }

        return string(abi.encodePacked(
            '<svg xmlns="<http://www.w3.org/2000/svg>" viewBox="0 0 100 100">',
            '<rect width="100" height="100" fill="#1a1a2e"/>',
            shapes,
            '</svg>'
        ));
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        require(_ownerOf(tokenId) != address(0), "Token does not exist");

        string memory svg = generateSVG(tokenId);
        string memory svgBase64 = Base64.encode(bytes(svg));

        string memory json = string(abi.encodePacked(
            '{"name": "Generative #', tokenId.toString(), '",',
            '"description": "Generative art, fully on-chain",',
            '"image": "data:image/svg+xml;base64,', svgBase64, '"}'
        ));

        return string(abi.encodePacked(
            "data:application/json;base64,",
            Base64.encode(bytes(json))
        ));
    }
}

This pattern is similar to how Nouns works. Each token stores a seed, and the artwork is deterministically generated from that seed every time tokenURI is called. The SVG never exists in storage; it's computed on the fly.

Gas Costs: Flow vs Other Chains

Here's where Flow's advantage becomes concrete. Let's compare storing a 5KB SVG:

Chain Approximate Cost
Ethereum Mainnet $25 – $100+
Polygon $0.10 – $0.50
Base $0.05 – $0.20
Flow EVM $0.00000256

On Flow, you can mint an on-chain SVG NFT for less than a penny. This isn't a rounding error. It fundamentally changes what's economically viable.

Size Limits and Optimization

Even with cheap gas, there are practical limits. The transaction data limit is approximately 40KB, which constrains how large your SVGs can be. Some tips:

  1. Minify your SVGs: Remove unnecessary whitespace, comments, and metadata.
  2. Use shorthand: fill="#FFF" instead of fill="#FFFFFF".
  3. Simplify paths: Tools like SVGO can optimize path data.
  4. Consider generation: Generative approaches store parameters, not pixels.
  5. Reuse definitions: SVG <defs> and <use> elements reduce repetition.

A well-optimized SVG can represent surprisingly complex artwork in under 10KB.

Adding Traits

On-chain NFTs can also return on-chain traits. Marketplaces read these from the tokenURI response:

function tokenURI(uint256 tokenId) public view override returns (string memory) {
    require(_ownerOf(tokenId) != address(0), "Token does not exist");

    uint256 seed = _seeds[tokenId];
    uint256 numShapes = (seed % 5) + 3;
    string memory rarity = numShapes >= 6 ? "Rare" : "Common";

    string memory svg = generateSVG(tokenId);
    string memory svgBase64 = Base64.encode(bytes(svg));

    string memory json = string(abi.encodePacked(
        '{"name": "Generative #', tokenId.toString(), '",',
        '"description": "Generative art, fully on-chain",',
        '"image": "data:image/svg+xml;base64,', svgBase64, '",',
        '"attributes": [',
            '{"trait_type": "Shape Count", "value": ', numShapes.toString(), '},',
            '{"trait_type": "Rarity", "value": "', rarity, '"}',
        ']}'
    ));

    return string(abi.encodePacked(
        "data:application/json;base64,",
        Base64.encode(bytes(json))
    ));
}

Traits are computed from the same seed, ensuring consistency between the visual output and the metadata.

When to Use On-Chain SVGs

On-chain SVG NFTs make sense when:

  • Permanence matters: The art should outlive any company or infrastructure.
  • Simplicity is aesthetic: Your art style works well as vector graphics.
  • Generative is the concept: The art is defined by an algorithm, not a static file.
  • Composability is needed: Other contracts might want to read or build on your artwork.

They're less ideal for:

  • Photorealistic art: SVGs aren't great for photographs.
  • Highly detailed illustrations: Complex artwork might exceed size limits.
  • Existing raster assets: Converting raster to vector often looks worse.

Flow's low gas costs make on-chain SVG NFTs practical for the first time. What was once an expensive luxury reserved for high-profile generative projects is now accessible to any developer. Your NFT's artwork can live on-chain forever, rendered by any browser, with no external dependencies.

The technical pattern is straightforward: store SVG data (or seeds for generative art), encode it as base64, wrap it in JSON metadata, and return it from tokenURI. The economics are what changed. On Flow, this costs fractions of a cent instead of tens of dollars.

If you're building NFTs and permanence matters to you, on-chain SVGs on Flow are worth exploring.