The reason why it passes an empty string is that all of your content is optional or allowed to match 0 times:
^
- match beginning of string
:?
- optional colon
([a-fA-F0-9]{1,4}(:|.)?){0,8}
- matches 0 - 8 times (thus optional)
(:|::)?
- optional colon or double colon
([a-fA-F0-9]{1,4}(:|.)?){0,8}
- matches 0 - 8 times (thus optional)
$
- matches end of string
Thus a blank string is allowed, as your pattern allows a string to not match any of the optional parts.
Looking at RFC 4291, which defines the IP 6 specification, section 2.2 defines three methods of representing the address. It may be easiest, if you need to match all forms to define them separately and combine the separate regex's together, as in
^(regex_pattern1|regex_pattern2|regex_pattern3)$
where pattern1, for example, is (?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}
and patterns 2 and 3 are for the other cases (pattern 1 taken from Regular Expressions Cookbook, 2nd Edition (O'Reilly, 2012))
Or, even better (for readability), test them in serial (pseudocode follows),
if (matches pattern 1)
echo 'valid'
else if (matches pattern 2)
echo 'valid'
else if (matches pattern 3)
echo 'valid'
else
echo 'invalid'
See, also, this question for some more information.